From e6da773a365a089827bca2a7d512d16724b84fef Mon Sep 17 00:00:00 2001 From: Danial Mehrjerdi Date: Sat, 14 Jun 2025 14:28:45 +0200 Subject: [PATCH 1/9] Add verifier to pyth evm contract --- .../contracts/contracts/pyth/Pyth.sol | 4 +- .../contracts/pyth/PythAccumulator.sol | 10 ++- .../contracts/contracts/pyth/PythGetters.sol | 4 + .../contracts/pyth/PythGovernance.sol | 84 +++++++++++++++++-- .../pyth/PythGovernanceInstructions.sol | 20 ++++- .../contracts/contracts/pyth/PythSetters.sol | 4 + .../contracts/contracts/pyth/PythState.sol | 2 + .../contracts/pyth/PythUpgradable.sol | 6 +- .../ethereum/sdk/solidity/PythErrors.sol | 3 + .../sdk/solidity/abis/PythErrors.json | 5 ++ 10 files changed, 129 insertions(+), 13 deletions(-) diff --git a/target_chains/ethereum/contracts/contracts/pyth/Pyth.sol b/target_chains/ethereum/contracts/contracts/pyth/Pyth.sol index b4a1df3f14..8a3019294d 100644 --- a/target_chains/ethereum/contracts/contracts/pyth/Pyth.sol +++ b/target_chains/ethereum/contracts/contracts/pyth/Pyth.sol @@ -25,9 +25,11 @@ abstract contract Pyth is bytes32 governanceEmitterAddress, uint64 governanceInitialSequence, uint validTimePeriodSeconds, - uint singleUpdateFeeInWei + uint singleUpdateFeeInWei, + address verifier ) internal { setWormhole(wormhole); + setVerifier(verifier); if ( dataSourceEmitterChainIds.length != diff --git a/target_chains/ethereum/contracts/contracts/pyth/PythAccumulator.sol b/target_chains/ethereum/contracts/contracts/pyth/PythAccumulator.sol index b9bd83ba0a..e1628dbca2 100644 --- a/target_chains/ethereum/contracts/contracts/pyth/PythAccumulator.sol +++ b/target_chains/ethereum/contracts/contracts/pyth/PythAccumulator.sol @@ -37,7 +37,15 @@ abstract contract PythAccumulator is PythGetters, PythSetters, AbstractPyth { { bool valid; (vm, valid, ) = wormhole().parseAndVerifyVM(encodedVm); - if (!valid) revert PythErrors.InvalidWormholeVaa(); + if (!valid) { + if (address(verifier()) == address(0)) { + revert PythErrors.InvalidUpdateData(); + } + (vm, valid, ) = verifier().parseAndVerifyVM(encodedVm); + if (!valid) { + revert PythErrors.InvalidUpdateData(); + } + } } if (!isValidDataSource(vm.emitterChainId, vm.emitterAddress)) diff --git a/target_chains/ethereum/contracts/contracts/pyth/PythGetters.sol b/target_chains/ethereum/contracts/contracts/pyth/PythGetters.sol index e0146da190..d0f05501e7 100644 --- a/target_chains/ethereum/contracts/contracts/pyth/PythGetters.sol +++ b/target_chains/ethereum/contracts/contracts/pyth/PythGetters.sol @@ -95,4 +95,8 @@ contract PythGetters is PythState { function transactionFeeInWei() public view returns (uint) { return _state.transactionFeeInWei; } + + function verifier() public view returns (IWormhole) { + return IWormhole(_state.verifier); + } } diff --git a/target_chains/ethereum/contracts/contracts/pyth/PythGovernance.sol b/target_chains/ethereum/contracts/contracts/pyth/PythGovernance.sol index d3f4d5dc09..c349ecdc89 100644 --- a/target_chains/ethereum/contracts/contracts/pyth/PythGovernance.sol +++ b/target_chains/ethereum/contracts/contracts/pyth/PythGovernance.sol @@ -40,15 +40,33 @@ abstract contract PythGovernance is ); event TransactionFeeSet(uint oldFee, uint newFee); event FeeWithdrawn(address targetAddress, uint fee); + event VerifierAddressSet( + address oldVerifierAddress, + address newVerifierAddress + ); - function verifyGovernanceVM( + // Check whether the encodedVM is signed by either the Wormhole or the Verifier. + function validateVm( bytes memory encodedVM - ) internal returns (IWormhole.VM memory parsedVM) { + ) internal view returns (IWormhole.VM memory parsedVM) { (IWormhole.VM memory vm, bool valid, ) = wormhole().parseAndVerifyVM( encodedVM ); + if (valid) return vm; - if (!valid) revert PythErrors.InvalidWormholeVaa(); + if (address(verifier()) != address(0)) { + (IWormhole.VM memory vmv, bool validv, ) = verifier() + .parseAndVerifyVM(encodedVM); + if (validv) return vmv; + } + + revert PythErrors.InvalidWormholeVaa(); + } + + function verifyGovernanceVM( + bytes memory encodedVM + ) internal returns (IWormhole.VM memory parsedVM) { + IWormhole.VM memory vm = validateVm(encodedVM); if (!isValidGovernanceDataSource(vm.emitterChainId, vm.emitterAddress)) revert PythErrors.InvalidGovernanceDataSource(); @@ -105,6 +123,13 @@ abstract contract PythGovernance is setTransactionFee(parseSetTransactionFeePayload(gi.payload)); } else if (gi.action == GovernanceAction.WithdrawFee) { withdrawFee(parseWithdrawFeePayload(gi.payload)); + } else if (gi.action == GovernanceAction.SetVerifierAddress) { + if (gi.targetChainId == 0) + revert PythErrors.InvalidGovernanceTarget(); + setVerifierAddress( + parseSetVerifierAddressPayload(gi.payload), + encodedVM + ); } else { revert PythErrors.InvalidGovernanceMessage(); } @@ -131,11 +156,8 @@ abstract contract PythGovernance is // Make sure the claimVaa is a valid VAA with RequestGovernanceDataSourceTransfer governance message // If it's valid then its emitter can take over the governance from the current emitter. // The VAA is checked here to ensure that the new governance data source is valid and can send message - // through wormhole. - (IWormhole.VM memory vm, bool valid, ) = wormhole().parseAndVerifyVM( - payload.claimVaa - ); - if (!valid) revert PythErrors.InvalidWormholeVaa(); + // through wormhole or verifier. + IWormhole.VM memory vm = validateVm(payload.claimVaa); GovernanceInstruction memory gi = parseGovernanceInstruction( vm.payload @@ -210,6 +232,8 @@ abstract contract PythGovernance is emit ValidPeriodSet(oldValidPeriod, validTimePeriodSeconds()); } + // If the VAA was created by Verifier, this will revert, + // because it assumes that the new Wormhole is able to parse and verify the governance VAA. function setWormholeAddress( SetWormholeAddressPayload memory payload, bytes memory encodedVM @@ -270,4 +294,48 @@ abstract contract PythGovernance is emit FeeWithdrawn(payload.targetAddress, payload.fee); } + + // If the VAA was created by Wormhole, this will revert, + // because it assumes that the new Verifier is able to parse and verify the governance VAA. + function setVerifierAddress( + SetVerifierAddressPayload memory payload, + bytes memory encodedVM + ) internal { + address oldVerifierAddress = address(verifier()); + setVerifier(payload.newVerifierAddress); + + // We want to verify that the new verifier address is valid, so we make sure that it can + // parse and verify the same governance VAA that is used to set it. + (IWormhole.VM memory vm, bool valid, ) = verifier().parseAndVerifyVM( + encodedVM + ); + + if (!valid) revert PythErrors.InvalidGovernanceMessage(); + + if (!isValidGovernanceDataSource(vm.emitterChainId, vm.emitterAddress)) + revert PythErrors.InvalidGovernanceMessage(); + + if (vm.sequence != lastExecutedGovernanceSequence()) + revert PythErrors.InvalidVerifierAddressToSet(); + + GovernanceInstruction memory gi = parseGovernanceInstruction( + vm.payload + ); + + if (gi.action != GovernanceAction.SetVerifierAddress) + revert PythErrors.InvalidVerifierAddressToSet(); + + // Purposefully, we don't check whether the chainId is the same as the current chainId because + // we might want to change the chain id of the verifier contract. + + // The following check is not necessary for security, but is a sanity check that the new verifier + // contract parses the payload correctly. + SetVerifierAddressPayload + memory newPayload = parseSetVerifierAddressPayload(gi.payload); + + if (newPayload.newVerifierAddress != payload.newVerifierAddress) + revert PythErrors.InvalidVerifierAddressToSet(); + + emit VerifierAddressSet(oldVerifierAddress, address(verifier())); + } } diff --git a/target_chains/ethereum/contracts/contracts/pyth/PythGovernanceInstructions.sol b/target_chains/ethereum/contracts/contracts/pyth/PythGovernanceInstructions.sol index 9290bd4855..6adefd5bd0 100644 --- a/target_chains/ethereum/contracts/contracts/pyth/PythGovernanceInstructions.sol +++ b/target_chains/ethereum/contracts/contracts/pyth/PythGovernanceInstructions.sol @@ -37,7 +37,8 @@ contract PythGovernanceInstructions { SetWormholeAddress, // 6 SetFeeInToken, // 7 - No-op for EVM chains SetTransactionFee, // 8 - WithdrawFee // 9 + WithdrawFee, // 9 + SetVerifierAddress // 10 } struct GovernanceInstruction { @@ -90,6 +91,10 @@ contract PythGovernanceInstructions { uint256 fee; } + struct SetVerifierAddressPayload { + address newVerifierAddress; + } + /// @dev Parse a GovernanceInstruction function parseGovernanceInstruction( bytes memory encodedInstruction @@ -272,4 +277,17 @@ contract PythGovernanceInstructions { if (encodedPayload.length != index) revert PythErrors.InvalidGovernanceMessage(); } + + /// @dev Parse a UpdateVerifierAddressPayload (action 10) with minimal validation + function parseSetVerifierAddressPayload( + bytes memory encodedPayload + ) public pure returns (SetVerifierAddressPayload memory sv) { + uint index = 0; + + sv.newVerifierAddress = address(encodedPayload.toAddress(index)); + index += 20; + + if (encodedPayload.length != index) + revert PythErrors.InvalidGovernanceMessage(); + } } diff --git a/target_chains/ethereum/contracts/contracts/pyth/PythSetters.sol b/target_chains/ethereum/contracts/contracts/pyth/PythSetters.sol index 849fc7659f..8dc0f463e6 100644 --- a/target_chains/ethereum/contracts/contracts/pyth/PythSetters.sol +++ b/target_chains/ethereum/contracts/contracts/pyth/PythSetters.sol @@ -52,4 +52,8 @@ contract PythSetters is PythState, IPythEvents { function setTransactionFeeInWei(uint fee) internal { _state.transactionFeeInWei = fee; } + + function setVerifier(address vf) internal { + _state.verifier = payable(vf); + } } diff --git a/target_chains/ethereum/contracts/contracts/pyth/PythState.sol b/target_chains/ethereum/contracts/contracts/pyth/PythState.sol index a860a4341b..6a0f2927fb 100644 --- a/target_chains/ethereum/contracts/contracts/pyth/PythState.sol +++ b/target_chains/ethereum/contracts/contracts/pyth/PythState.sol @@ -40,6 +40,8 @@ contract PythStorage { mapping(bytes32 => PythInternalStructs.PriceInfo) latestPriceInfo; // Fee charged per transaction, in addition to per-update fees uint transactionFeeInWei; + // Verifier address for verifying VAA signatures + address verifier; } } diff --git a/target_chains/ethereum/contracts/contracts/pyth/PythUpgradable.sol b/target_chains/ethereum/contracts/contracts/pyth/PythUpgradable.sol index 7f9a2a22c4..adf718b162 100644 --- a/target_chains/ethereum/contracts/contracts/pyth/PythUpgradable.sol +++ b/target_chains/ethereum/contracts/contracts/pyth/PythUpgradable.sol @@ -28,7 +28,8 @@ contract PythUpgradable is bytes32 governanceEmitterAddress, uint64 governanceInitialSequence, uint validTimePeriodSeconds, - uint singleUpdateFeeInWei + uint singleUpdateFeeInWei, + address verifier ) public initializer { __Ownable_init(); __UUPSUpgradeable_init(); @@ -41,7 +42,8 @@ contract PythUpgradable is governanceEmitterAddress, governanceInitialSequence, validTimePeriodSeconds, - singleUpdateFeeInWei + singleUpdateFeeInWei, + verifier ); renounceOwnership(); diff --git a/target_chains/ethereum/sdk/solidity/PythErrors.sol b/target_chains/ethereum/sdk/solidity/PythErrors.sol index ad98f11e30..0ca223d75a 100644 --- a/target_chains/ethereum/sdk/solidity/PythErrors.sol +++ b/target_chains/ethereum/sdk/solidity/PythErrors.sol @@ -49,4 +49,7 @@ library PythErrors { error InvalidTwapUpdateData(); // The twap update data set is invalid. error InvalidTwapUpdateDataSet(); + // The verifier address to set in SetVerifierAddress governance is invalid. + // Signature: 0xab8af376 + error InvalidVerifierAddressToSet(); } diff --git a/target_chains/ethereum/sdk/solidity/abis/PythErrors.json b/target_chains/ethereum/sdk/solidity/abis/PythErrors.json index def11cb07a..dbb8a0b5b1 100644 --- a/target_chains/ethereum/sdk/solidity/abis/PythErrors.json +++ b/target_chains/ethereum/sdk/solidity/abis/PythErrors.json @@ -44,6 +44,11 @@ "name": "InvalidUpdateDataSource", "type": "error" }, + { + "inputs": [], + "name": "InvalidVerifierAddressToSet", + "type": "error" + }, { "inputs": [], "name": "InvalidWormholeAddressToSet", From 64786b9b456f0b3b6f42292bcf2c69f638006161 Mon Sep 17 00:00:00 2001 From: Danial Mehrjerdi Date: Mon, 16 Jun 2025 08:44:27 +0200 Subject: [PATCH 2/9] Fix tests --- target_chains/ethereum/contracts/contracts/pyth/Pyth.sol | 4 +--- .../ethereum/contracts/contracts/pyth/PythUpgradable.sol | 6 ++---- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/target_chains/ethereum/contracts/contracts/pyth/Pyth.sol b/target_chains/ethereum/contracts/contracts/pyth/Pyth.sol index 8a3019294d..b4a1df3f14 100644 --- a/target_chains/ethereum/contracts/contracts/pyth/Pyth.sol +++ b/target_chains/ethereum/contracts/contracts/pyth/Pyth.sol @@ -25,11 +25,9 @@ abstract contract Pyth is bytes32 governanceEmitterAddress, uint64 governanceInitialSequence, uint validTimePeriodSeconds, - uint singleUpdateFeeInWei, - address verifier + uint singleUpdateFeeInWei ) internal { setWormhole(wormhole); - setVerifier(verifier); if ( dataSourceEmitterChainIds.length != diff --git a/target_chains/ethereum/contracts/contracts/pyth/PythUpgradable.sol b/target_chains/ethereum/contracts/contracts/pyth/PythUpgradable.sol index adf718b162..7f9a2a22c4 100644 --- a/target_chains/ethereum/contracts/contracts/pyth/PythUpgradable.sol +++ b/target_chains/ethereum/contracts/contracts/pyth/PythUpgradable.sol @@ -28,8 +28,7 @@ contract PythUpgradable is bytes32 governanceEmitterAddress, uint64 governanceInitialSequence, uint validTimePeriodSeconds, - uint singleUpdateFeeInWei, - address verifier + uint singleUpdateFeeInWei ) public initializer { __Ownable_init(); __UUPSUpgradeable_init(); @@ -42,8 +41,7 @@ contract PythUpgradable is governanceEmitterAddress, governanceInitialSequence, validTimePeriodSeconds, - singleUpdateFeeInWei, - verifier + singleUpdateFeeInWei ); renounceOwnership(); From 0e370d03248785e547de6912352d904801baaa51 Mon Sep 17 00:00:00 2001 From: Danial Mehrjerdi Date: Mon, 16 Jun 2025 15:07:18 +0100 Subject: [PATCH 3/9] Use only wh for governance messages --- .../contracts/pyth/PythGovernance.sol | 64 +++---------------- 1 file changed, 8 insertions(+), 56 deletions(-) diff --git a/target_chains/ethereum/contracts/contracts/pyth/PythGovernance.sol b/target_chains/ethereum/contracts/contracts/pyth/PythGovernance.sol index c349ecdc89..37764844d7 100644 --- a/target_chains/ethereum/contracts/contracts/pyth/PythGovernance.sol +++ b/target_chains/ethereum/contracts/contracts/pyth/PythGovernance.sol @@ -45,28 +45,14 @@ abstract contract PythGovernance is address newVerifierAddress ); - // Check whether the encodedVM is signed by either the Wormhole or the Verifier. - function validateVm( + function verifyGovernanceVM( bytes memory encodedVM - ) internal view returns (IWormhole.VM memory parsedVM) { + ) internal returns (IWormhole.VM memory parsedVM) { (IWormhole.VM memory vm, bool valid, ) = wormhole().parseAndVerifyVM( encodedVM ); - if (valid) return vm; - if (address(verifier()) != address(0)) { - (IWormhole.VM memory vmv, bool validv, ) = verifier() - .parseAndVerifyVM(encodedVM); - if (validv) return vmv; - } - - revert PythErrors.InvalidWormholeVaa(); - } - - function verifyGovernanceVM( - bytes memory encodedVM - ) internal returns (IWormhole.VM memory parsedVM) { - IWormhole.VM memory vm = validateVm(encodedVM); + if (!valid) revert PythErrors.InvalidWormholeVaa(); if (!isValidGovernanceDataSource(vm.emitterChainId, vm.emitterAddress)) revert PythErrors.InvalidGovernanceDataSource(); @@ -156,8 +142,11 @@ abstract contract PythGovernance is // Make sure the claimVaa is a valid VAA with RequestGovernanceDataSourceTransfer governance message // If it's valid then its emitter can take over the governance from the current emitter. // The VAA is checked here to ensure that the new governance data source is valid and can send message - // through wormhole or verifier. - IWormhole.VM memory vm = validateVm(payload.claimVaa); + // through wormhole. + (IWormhole.VM memory vm, bool valid, ) = wormhole().parseAndVerifyVM( + payload.claimVaa + ); + if (!valid) revert PythErrors.InvalidWormholeVaa(); GovernanceInstruction memory gi = parseGovernanceInstruction( vm.payload @@ -232,8 +221,6 @@ abstract contract PythGovernance is emit ValidPeriodSet(oldValidPeriod, validTimePeriodSeconds()); } - // If the VAA was created by Verifier, this will revert, - // because it assumes that the new Wormhole is able to parse and verify the governance VAA. function setWormholeAddress( SetWormholeAddressPayload memory payload, bytes memory encodedVM @@ -295,47 +282,12 @@ abstract contract PythGovernance is emit FeeWithdrawn(payload.targetAddress, payload.fee); } - // If the VAA was created by Wormhole, this will revert, - // because it assumes that the new Verifier is able to parse and verify the governance VAA. function setVerifierAddress( SetVerifierAddressPayload memory payload, bytes memory encodedVM ) internal { address oldVerifierAddress = address(verifier()); setVerifier(payload.newVerifierAddress); - - // We want to verify that the new verifier address is valid, so we make sure that it can - // parse and verify the same governance VAA that is used to set it. - (IWormhole.VM memory vm, bool valid, ) = verifier().parseAndVerifyVM( - encodedVM - ); - - if (!valid) revert PythErrors.InvalidGovernanceMessage(); - - if (!isValidGovernanceDataSource(vm.emitterChainId, vm.emitterAddress)) - revert PythErrors.InvalidGovernanceMessage(); - - if (vm.sequence != lastExecutedGovernanceSequence()) - revert PythErrors.InvalidVerifierAddressToSet(); - - GovernanceInstruction memory gi = parseGovernanceInstruction( - vm.payload - ); - - if (gi.action != GovernanceAction.SetVerifierAddress) - revert PythErrors.InvalidVerifierAddressToSet(); - - // Purposefully, we don't check whether the chainId is the same as the current chainId because - // we might want to change the chain id of the verifier contract. - - // The following check is not necessary for security, but is a sanity check that the new verifier - // contract parses the payload correctly. - SetVerifierAddressPayload - memory newPayload = parseSetVerifierAddressPayload(gi.payload); - - if (newPayload.newVerifierAddress != payload.newVerifierAddress) - revert PythErrors.InvalidVerifierAddressToSet(); - emit VerifierAddressSet(oldVerifierAddress, address(verifier())); } } From 9254cbc6e05031b03aacea7c19a702702489173e Mon Sep 17 00:00:00 2001 From: Danial Mehrjerdi Date: Mon, 16 Jun 2025 15:18:10 +0100 Subject: [PATCH 4/9] Refactor --- .../contracts/contracts/pyth/PythGovernance.sol | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/target_chains/ethereum/contracts/contracts/pyth/PythGovernance.sol b/target_chains/ethereum/contracts/contracts/pyth/PythGovernance.sol index 37764844d7..107141dac5 100644 --- a/target_chains/ethereum/contracts/contracts/pyth/PythGovernance.sol +++ b/target_chains/ethereum/contracts/contracts/pyth/PythGovernance.sol @@ -110,12 +110,7 @@ abstract contract PythGovernance is } else if (gi.action == GovernanceAction.WithdrawFee) { withdrawFee(parseWithdrawFeePayload(gi.payload)); } else if (gi.action == GovernanceAction.SetVerifierAddress) { - if (gi.targetChainId == 0) - revert PythErrors.InvalidGovernanceTarget(); - setVerifierAddress( - parseSetVerifierAddressPayload(gi.payload), - encodedVM - ); + setVerifierAddress(parseSetVerifierAddressPayload(gi.payload)); } else { revert PythErrors.InvalidGovernanceMessage(); } @@ -283,8 +278,7 @@ abstract contract PythGovernance is } function setVerifierAddress( - SetVerifierAddressPayload memory payload, - bytes memory encodedVM + SetVerifierAddressPayload memory payload ) internal { address oldVerifierAddress = address(verifier()); setVerifier(payload.newVerifierAddress); From 9854e9cad49c32a4cdb4a7e60ffb1f5b4c83d1b1 Mon Sep 17 00:00:00 2001 From: Danial Mehrjerdi Date: Mon, 16 Jun 2025 15:27:31 +0100 Subject: [PATCH 5/9] Add set verifier governance action test --- .../contracts/forge-test/PythGovernance.t.sol | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/target_chains/ethereum/contracts/forge-test/PythGovernance.t.sol b/target_chains/ethereum/contracts/forge-test/PythGovernance.t.sol index 148f507088..c0def09016 100644 --- a/target_chains/ethereum/contracts/forge-test/PythGovernance.t.sol +++ b/target_chains/ethereum/contracts/forge-test/PythGovernance.t.sol @@ -267,6 +267,34 @@ contract PythGovernanceTest is assertEq(address(PythGetters(address(pyth)).wormhole()), newWormhole); } + function testSetVerifierAddress() public { + // Deploy a new verifier contract + address newVerifier = address(setUpWormholeReceiver(1)); + + // Create governance VAA to set new verifier address + bytes memory data = abi.encodePacked( + MAGIC, + uint8(GovernanceModule.Target), + uint8(GovernanceAction.SetVerifierAddress), + TARGET_CHAIN_ID, // Target chain ID + newVerifier // New verifier address + ); + + bytes memory vaa = encodeAndSignMessage( + data, + TEST_GOVERNANCE_CHAIN_ID, + TEST_GOVERNANCE_EMITTER, + 1 + ); + + address oldVerifier = address(PythGetters(address(pyth)).verifier()); + vm.expectEmit(true, true, true, true); + emit VerifierAddressSet(oldVerifier, newVerifier); + + PythGovernance(address(pyth)).executeGovernanceInstruction(vaa); + assertEq(address(PythGetters(address(pyth)).verifier()), newVerifier); + } + function testTransferGovernanceDataSource() public { uint16 newEmitterChain = 2; bytes32 newEmitterAddress = 0x0000000000000000000000000000000000000000000000000000000000001111; @@ -706,4 +734,8 @@ contract PythGovernanceTest is ); event TransactionFeeSet(uint oldFee, uint newFee); event FeeWithdrawn(address recipient, uint256 fee); + event VerifierAddressSet( + address oldVerifierAddress, + address newVerifierAddress + ); } From 09f25290f56b009f27bb4ed5c71b549ac9c09b37 Mon Sep 17 00:00:00 2001 From: Danial Mehrjerdi Date: Tue, 17 Jun 2025 14:36:47 +0100 Subject: [PATCH 6/9] Add signer to pyth header --- .../contracts/contracts/pyth/Pyth.sol | 26 +++---- .../contracts/pyth/PythAccumulator.sol | 73 ++++++++++++------- .../contracts/forge-test/Executor.t.sol | 2 +- .../contracts/forge-test/GasBenchmark.t.sol | 10 ++- .../contracts/forge-test/Pyth.Aave.t.sol | 2 +- .../Pyth.WormholeMerkleAccumulator.t.sol | 35 +++++---- .../ethereum/contracts/forge-test/Pyth.t.sol | 4 +- .../contracts/forge-test/PythGovernance.t.sol | 18 ++--- .../forge-test/VerificationExperiments.t.sol | 18 +++-- .../forge-test/utils/PythTestUtils.t.sol | 47 ++++++++++-- .../forge-test/utils/WormholeTestUtils.t.sol | 39 +++------- .../ethereum/sdk/solidity/PythErrors.sol | 3 + .../sdk/solidity/abis/PythErrors.json | 5 ++ 13 files changed, 163 insertions(+), 119 deletions(-) diff --git a/target_chains/ethereum/contracts/contracts/pyth/Pyth.sol b/target_chains/ethereum/contracts/contracts/pyth/Pyth.sol index b4a1df3f14..4e35ace54f 100644 --- a/target_chains/ethereum/contracts/contracts/pyth/Pyth.sol +++ b/target_chains/ethereum/contracts/contracts/pyth/Pyth.sol @@ -104,8 +104,9 @@ abstract contract Pyth is ) { ( uint offset, - UpdateType updateType - ) = extractUpdateTypeFromAccumulatorHeader(updateData[i]); + UpdateType updateType, + + ) = extractAccumulatorHeaderDetails(updateData[i]); if (updateType != UpdateType.WormholeMerkle) { revert PythErrors.InvalidUpdateData(); } @@ -135,8 +136,9 @@ abstract contract Pyth is ) { ( uint offset, - UpdateType updateType - ) = extractUpdateTypeFromAccumulatorHeader(updateData[0]); + UpdateType updateType, + + ) = extractAccumulatorHeaderDetails(updateData[0]); if (updateType != UpdateType.WormholeMerkle) { revert PythErrors.InvalidUpdateData(); } @@ -273,9 +275,10 @@ abstract contract Pyth is } uint offset; + Signer signer; { UpdateType updateType; - (offset, updateType) = extractUpdateTypeFromAccumulatorHeader( + (offset, updateType, signer) = extractAccumulatorHeaderDetails( singleUpdateData ); @@ -293,10 +296,7 @@ abstract contract Pyth is merkleData.numUpdates, encoded, merkleData.slot - ) = extractWormholeMerkleHeaderDigestAndNumUpdatesAndEncodedAndSlotFromAccumulatorUpdate( - singleUpdateData, - offset - ); + ) = extractWormholeMerkleHeader(singleUpdateData, signer, offset); // Process each update within the Merkle proof for (uint j = 0; j < merkleData.numUpdates; j++) { @@ -435,12 +435,13 @@ abstract contract Pyth is ) { UpdateType updateType; + Signer signer; uint offset; bytes20 digest; uint8 numUpdates; bytes calldata encoded; // Extract and validate the header for start data - (offset, updateType) = extractUpdateTypeFromAccumulatorHeader( + (offset, updateType, signer) = extractAccumulatorHeaderDetails( updateData ); @@ -455,10 +456,7 @@ abstract contract Pyth is encoded, // slot ignored - ) = extractWormholeMerkleHeaderDigestAndNumUpdatesAndEncodedAndSlotFromAccumulatorUpdate( - updateData, - offset - ); + ) = extractWormholeMerkleHeader(updateData, signer, offset); // Add additional validation before extracting TWAP price info if (offset >= updateData.length) { diff --git a/target_chains/ethereum/contracts/contracts/pyth/PythAccumulator.sol b/target_chains/ethereum/contracts/contracts/pyth/PythAccumulator.sol index e1628dbca2..7d59ddd3fd 100644 --- a/target_chains/ethereum/contracts/contracts/pyth/PythAccumulator.sol +++ b/target_chains/ethereum/contracts/contracts/pyth/PythAccumulator.sol @@ -29,32 +29,44 @@ abstract contract PythAccumulator is PythGetters, PythSetters, AbstractPyth { TwapPriceFeed } + enum Signer { + Wormhole, + Verifier + } + // This method is also used by batch attestation but moved here // as the batch attestation will deprecate soon. function parseAndVerifyPythVM( - bytes calldata encodedVm + bytes calldata encodedVm, + Signer signer ) internal view returns (IWormhole.VM memory vm) { - { - bool valid; + bool valid; + if (signer == Signer.Wormhole) { (vm, valid, ) = wormhole().parseAndVerifyVM(encodedVm); - if (!valid) { - if (address(verifier()) == address(0)) { - revert PythErrors.InvalidUpdateData(); - } - (vm, valid, ) = verifier().parseAndVerifyVM(encodedVm); - if (!valid) { - revert PythErrors.InvalidUpdateData(); - } - } + } else if (signer == Signer.Verifier) { + if (address(verifier()) == address(0)) + revert PythErrors.InvalidUpdateData(); + (vm, valid, ) = verifier().parseAndVerifyVM(encodedVm); + } else { + revert PythErrors.InvalidSigner(); } - if (!isValidDataSource(vm.emitterChainId, vm.emitterAddress)) + if (!valid) { + revert PythErrors.InvalidUpdateData(); + } + + if (!isValidDataSource(vm.emitterChainId, vm.emitterAddress)) { revert PythErrors.InvalidUpdateDataSource(); + } } - function extractUpdateTypeFromAccumulatorHeader( + function extractAccumulatorHeaderDetails( bytes calldata accumulatorUpdate - ) internal pure returns (uint offset, UpdateType updateType) { + ) + internal + pure + returns (uint offset, UpdateType updateType, Signer signer) + { unchecked { offset = 0; @@ -98,13 +110,14 @@ abstract contract PythAccumulator is PythGetters, PythSetters, AbstractPyth { ); offset += 1; - // We use another offset for the trailing header and in the end add the - // offset by trailingHeaderSize to skip the future headers. - // - // An example would be like this: - // uint trailingHeaderOffset = offset - // uint x = UnsafeBytesLib.ToUint8(accumulatorUpdate, trailingHeaderOffset) - // trailingHeaderOffset += 1 + uint trailingHeaderOffset = offset; + signer = Signer( + UnsafeCalldataBytesLib.toUint8( + accumulatorUpdate, + trailingHeaderOffset + ) + ); + trailingHeaderOffset += 1; offset += trailingHeaderSize; } @@ -120,8 +133,9 @@ abstract contract PythAccumulator is PythGetters, PythSetters, AbstractPyth { } } - function extractWormholeMerkleHeaderDigestAndNumUpdatesAndEncodedAndSlotFromAccumulatorUpdate( + function extractWormholeMerkleHeader( bytes calldata accumulatorUpdate, + Signer signer, uint encodedOffset ) internal @@ -156,7 +170,8 @@ abstract contract PythAccumulator is PythGetters, PythSetters, AbstractPyth { encoded, offset, whProofSize - ) + ), + signer ); offset += whProofSize; @@ -179,7 +194,7 @@ abstract contract PythAccumulator is PythGetters, PythSetters, AbstractPyth { UpdateType updateType = UpdateType( UnsafeBytesLib.toUint8(encodedPayload, payloadOffset) ); - ++payloadOffset; + payloadOffset += 1; if (updateType != UpdateType.WormholeMerkle) revert PythErrors.InvalidUpdateData(); @@ -468,8 +483,9 @@ abstract contract PythAccumulator is PythGetters, PythSetters, AbstractPyth { ) internal returns (uint8 numUpdates) { ( uint encodedOffset, - UpdateType updateType - ) = extractUpdateTypeFromAccumulatorHeader(accumulatorUpdate); + UpdateType updateType, + Signer signer + ) = extractAccumulatorHeaderDetails(accumulatorUpdate); if (updateType != UpdateType.WormholeMerkle) { revert PythErrors.InvalidUpdateData(); @@ -485,8 +501,9 @@ abstract contract PythAccumulator is PythGetters, PythSetters, AbstractPyth { numUpdates, encoded, slot - ) = extractWormholeMerkleHeaderDigestAndNumUpdatesAndEncodedAndSlotFromAccumulatorUpdate( + ) = extractWormholeMerkleHeader( accumulatorUpdate, + signer, encodedOffset ); diff --git a/target_chains/ethereum/contracts/forge-test/Executor.t.sol b/target_chains/ethereum/contracts/forge-test/Executor.t.sol index 99b30c0c16..56e0465043 100644 --- a/target_chains/ethereum/contracts/forge-test/Executor.t.sol +++ b/target_chains/ethereum/contracts/forge-test/Executor.t.sol @@ -8,7 +8,7 @@ import "../contracts/executor/ExecutorUpgradable.sol"; import "./utils/WormholeTestUtils.t.sol"; import "./utils/InvalidMagic.t.sol"; -contract ExecutorTest is Test, WormholeTestUtils { +contract ExecutorTest is Test, AbstractWormholeTestUtils { Wormhole public wormhole; ExecutorUpgradable public executor; ExecutorUpgradable public executor2; diff --git a/target_chains/ethereum/contracts/forge-test/GasBenchmark.t.sol b/target_chains/ethereum/contracts/forge-test/GasBenchmark.t.sol index 063a5c2e6a..12858c2b00 100644 --- a/target_chains/ethereum/contracts/forge-test/GasBenchmark.t.sol +++ b/target_chains/ethereum/contracts/forge-test/GasBenchmark.t.sol @@ -11,7 +11,7 @@ import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol"; import "./utils/WormholeTestUtils.t.sol"; import "./utils/PythTestUtils.t.sol"; -contract GasBenchmark is Test, WormholeTestUtils, PythTestUtils { +contract GasBenchmark is Test, PythTestUtils { // 19, current mainnet number of guardians, is used to have gas estimates // close to our mainnet transactions. uint8 constant NUM_GUARDIANS = 19; @@ -53,9 +53,11 @@ contract GasBenchmark is Test, WormholeTestUtils, PythTestUtils { uint randomSeed; function setUp() public { - address wormholeAddr = setUpWormholeReceiver(NUM_GUARDIANS); - wormhole = IWormhole(wormholeAddr); - pyth = IPyth(setUpPyth(wormholeAddr)); + WormholeTestUtils wormholeTestUtils = new WormholeTestUtils( + NUM_GUARDIANS + ); + wormhole = IWormhole(wormholeTestUtils.getWormholeReceiverAddr()); + pyth = IPyth(setUpPyth(wormholeTestUtils)); priceIds = new bytes32[](NUM_PRICES); priceIds[0] = bytes32( diff --git a/target_chains/ethereum/contracts/forge-test/Pyth.Aave.t.sol b/target_chains/ethereum/contracts/forge-test/Pyth.Aave.t.sol index ecb3b681d2..19b458bb9b 100644 --- a/target_chains/ethereum/contracts/forge-test/Pyth.Aave.t.sol +++ b/target_chains/ethereum/contracts/forge-test/Pyth.Aave.t.sol @@ -24,7 +24,7 @@ contract PythAaveTest is PythWormholeMerkleAccumulatorTest { uint constant VALID_TIME_PERIOD_SECS = 60; function setUp() public override { - pyth = IPyth(setUpPyth(setUpWormholeReceiver(1))); + pyth = IPyth(setUpPyth(new WormholeTestUtils(1))); assets = new address[](NUM_PRICE_FEEDS); PriceFeedMessage[] memory priceFeedMessages = generateRandomBoundedPriceFeedMessage( diff --git a/target_chains/ethereum/contracts/forge-test/Pyth.WormholeMerkleAccumulator.t.sol b/target_chains/ethereum/contracts/forge-test/Pyth.WormholeMerkleAccumulator.t.sol index c8d7d40997..261c6d7a1a 100644 --- a/target_chains/ethereum/contracts/forge-test/Pyth.WormholeMerkleAccumulator.t.sol +++ b/target_chains/ethereum/contracts/forge-test/Pyth.WormholeMerkleAccumulator.t.sol @@ -8,24 +8,20 @@ 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 "./utils/WormholeTestUtils.t.sol"; import "./utils/PythTestUtils.t.sol"; import "./utils/RandTestUtils.t.sol"; import "../contracts/libraries/MerkleTree.sol"; +import "./utils/WormholeTestUtils.t.sol"; -contract PythWormholeMerkleAccumulatorTest is - Test, - WormholeTestUtils, - PythTestUtils -{ +contract PythWormholeMerkleAccumulatorTest is Test, PythTestUtils { IPyth public pyth; // -1 is equal to 0xffffff which is the biggest uint if converted back uint64 constant MAX_UINT64 = uint64(int64(-1)); function setUp() public virtual { - pyth = IPyth(setUpPyth(setUpWormholeReceiver(1))); + pyth = IPyth(setUpPyth(new WormholeTestUtils(1))); } function assertPriceFeedMessageStored( @@ -530,15 +526,15 @@ contract PythWormholeMerkleAccumulatorTest is bytes memory wormholePayload; unchecked { wormholePayload = abi.encodePacked( - isNotMatch(forgeItem, "whMagic") + _wormholeTestUtils.isNotMatch(forgeItem, "whMagic") ? uint32(0x41555756) : uint32(0x41555750), - isNotMatch(forgeItem, "whUpdateType") + _wormholeTestUtils.isNotMatch(forgeItem, "whUpdateType") ? uint8(PythAccumulator.UpdateType.WormholeMerkle) : uint8(PythAccumulator.UpdateType.WormholeMerkle) + 1, uint64(0), // Slot, not used in target networks uint32(0), // Storage index, not used in target networks - isNotMatch(forgeItem, "rootDigest") + _wormholeTestUtils.isNotMatch(forgeItem, "rootDigest") ? rootDigest : bytes20(uint160(rootDigest) + 1) ); @@ -546,26 +542,29 @@ contract PythWormholeMerkleAccumulatorTest is bytes memory wormholeMerkleVaa = generateVaa( 0, - isNotMatch(forgeItem, "whSourceChain") + _wormholeTestUtils.isNotMatch(forgeItem, "whSourceChain") ? SOURCE_EMITTER_CHAIN_ID : SOURCE_EMITTER_CHAIN_ID + 1, - isNotMatch(forgeItem, "whSourceAddress") + _wormholeTestUtils.isNotMatch(forgeItem, "whSourceAddress") ? SOURCE_EMITTER_ADDRESS : bytes32( 0x71f8dcb863d176e2c420ad6610cf687359612b6fb392e0642b0ca6b1f186aa00 ), 0, wormholePayload, - 1 // num signers + 1, // num signers + false ); updateData = new bytes[](1); updateData[0] = abi.encodePacked( - isNotMatch(forgeItem, "headerMagic") + _wormholeTestUtils.isNotMatch(forgeItem, "headerMagic") ? uint32(0x504e4155) : uint32(0x504e4150), // PythAccumulator.ACCUMULATOR_MAGIC - isNotMatch(forgeItem, "headerMajorVersion") ? uint8(1) : uint8(2), // major version + _wormholeTestUtils.isNotMatch(forgeItem, "headerMajorVersion") + ? uint8(1) + : uint8(2), // major version uint8(0), // minor version uint8(0), // trailing header size uint8(PythAccumulator.UpdateType.WormholeMerkle), @@ -575,11 +574,15 @@ contract PythWormholeMerkleAccumulatorTest is ); for (uint i = 0; i < priceFeedMessages.length; i++) { + bytes memory proof = proofs[0]; + if (_wormholeTestUtils.isNotMatch(forgeItem, "proofItem")) { + proof = proofs[i]; + } updateData[0] = abi.encodePacked( updateData[0], uint16(encodedPriceFeedMessages[i].length), encodedPriceFeedMessages[i], - isNotMatch(forgeItem, "proofItem") ? proofs[i] : proofs[0] + proof ); } diff --git a/target_chains/ethereum/contracts/forge-test/Pyth.t.sol b/target_chains/ethereum/contracts/forge-test/Pyth.t.sol index e7b41accff..431fa9d8e7 100644 --- a/target_chains/ethereum/contracts/forge-test/Pyth.t.sol +++ b/target_chains/ethereum/contracts/forge-test/Pyth.t.sol @@ -13,7 +13,7 @@ import "./utils/PythTestUtils.t.sol"; import "./utils/RandTestUtils.t.sol"; import "forge-std/console.sol"; -contract PythTest is Test, WormholeTestUtils, PythTestUtils { +contract PythTest is Test, PythTestUtils { IPyth public pyth; // -1 is equal to 0xffffff which is the biggest uint if converted back @@ -32,7 +32,7 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils { bytes32[2] basePriceIds; function setUp() public { - pyth = IPyth(setUpPyth(setUpWormholeReceiver(NUM_GUARDIAN_SIGNERS))); + pyth = IPyth(setUpPyth(new WormholeTestUtils(NUM_GUARDIAN_SIGNERS))); // Initialize base TWAP messages for two price feeds basePriceIds[0] = bytes32(uint256(1)); diff --git a/target_chains/ethereum/contracts/forge-test/PythGovernance.t.sol b/target_chains/ethereum/contracts/forge-test/PythGovernance.t.sol index c0def09016..859178a53a 100644 --- a/target_chains/ethereum/contracts/forge-test/PythGovernance.t.sol +++ b/target_chains/ethereum/contracts/forge-test/PythGovernance.t.sol @@ -33,12 +33,7 @@ import "./utils/WormholeTestUtils.t.sol"; import "./utils/PythTestUtils.t.sol"; import "./utils/RandTestUtils.t.sol"; -contract PythGovernanceTest is - Test, - WormholeTestUtils, - PythTestUtils, - PythGovernanceInstructions -{ +contract PythGovernanceTest is Test, PythTestUtils, PythGovernanceInstructions { using BytesLib for bytes; IPyth public pyth; @@ -53,7 +48,7 @@ contract PythGovernanceTest is uint16 constant TARGET_CHAIN_ID = 2; function setUp() public { - pyth = IPyth(setUpPyth(setUpWormholeReceiver(1))); + pyth = IPyth(setUpPyth(new WormholeTestUtils(1))); } function testNoOwner() public { @@ -241,7 +236,8 @@ contract PythGovernanceTest is function testSetWormholeAddress() public { // Deploy a new wormhole contract - address newWormhole = address(setUpWormholeReceiver(1)); + address newWormhole = new WormholeTestUtils(1) + .getWormholeReceiverAddr(); // Create governance VAA to set new wormhole address bytes memory data = abi.encodePacked( @@ -269,7 +265,8 @@ contract PythGovernanceTest is function testSetVerifierAddress() public { // Deploy a new verifier contract - address newVerifier = address(setUpWormholeReceiver(1)); + address newVerifier = new WormholeTestUtils(3) + .getWormholeReceiverAddr(); // Create governance VAA to set new verifier address bytes memory data = abi.encodePacked( @@ -692,7 +689,8 @@ contract PythGovernanceTest is emitterAddress, sequence, data, - numGuardians + numGuardians, + false ); } diff --git a/target_chains/ethereum/contracts/forge-test/VerificationExperiments.t.sol b/target_chains/ethereum/contracts/forge-test/VerificationExperiments.t.sol index eb9e852298..b83ea37e5e 100644 --- a/target_chains/ethereum/contracts/forge-test/VerificationExperiments.t.sol +++ b/target_chains/ethereum/contracts/forge-test/VerificationExperiments.t.sol @@ -16,7 +16,7 @@ import "./utils/PythTestUtils.t.sol"; import "./utils/RandTestUtils.t.sol"; // Experiments to measure the gas usage of different ways of verifying prices in the EVM contract. -contract VerificationExperiments is Test, WormholeTestUtils, PythTestUtils { +contract VerificationExperiments is Test, PythTestUtils { // 19, current mainnet number of guardians, is used to have gas estimates // close to our mainnet transactions. uint8 constant NUM_GUARDIANS = 19; @@ -65,8 +65,15 @@ contract VerificationExperiments is Test, WormholeTestUtils, PythTestUtils { uint64 sequence; function setUp() public { + WormholeTestUtils wormholeTestUtils = new WormholeTestUtils( + NUM_GUARDIAN_SIGNERS + ); + + // Just set it up for generating the VAA signatures. + setUpPyth(wormholeTestUtils); + address payable wormhole = payable( - setUpWormholeReceiver(NUM_GUARDIANS) + wormholeTestUtils.getWormholeReceiverAddr() ); // Deploy experimental contract @@ -93,7 +100,6 @@ contract VerificationExperiments is Test, WormholeTestUtils, PythTestUtils { 60, // Valid time period in seconds 1 // single update fee in wei ); - priceIds = new bytes32[](NUM_PRICES); priceIds[0] = bytes32( 0x1000000000000000000000000000000000000000000000000000000000000f00 @@ -127,7 +133,6 @@ contract VerificationExperiments is Test, WormholeTestUtils, PythTestUtils { ); freshPricesPublishTimes.push(publishTime); } - // Populate the contract with the initial prices ( cachedPricesUpdateData, @@ -144,7 +149,6 @@ contract VerificationExperiments is Test, WormholeTestUtils, PythTestUtils { ) = generateWormholeUpdateDataAndFee(freshPrices); // Generate the update payloads for the various verification systems - whMerkleUpdateDepth0 = generateSingleWhMerkleUpdate( priceIds[0], freshPrices[0], @@ -176,7 +180,6 @@ contract VerificationExperiments is Test, WormholeTestUtils, PythTestUtils { freshPrices[0], 8 ); - thresholdUpdate = generateThresholdUpdate(priceIds[0], freshPrices[0]); nativeUpdate = generateMessagePayload(priceIds[0], freshPrices[0]); @@ -265,7 +268,8 @@ contract VerificationExperiments is Test, WormholeTestUtils, PythTestUtils { PythTestUtils.SOURCE_EMITTER_ADDRESS, sequence, bytes.concat(root), - NUM_GUARDIAN_SIGNERS + NUM_GUARDIAN_SIGNERS, + false ); ++sequence; diff --git a/target_chains/ethereum/contracts/forge-test/utils/PythTestUtils.t.sol b/target_chains/ethereum/contracts/forge-test/utils/PythTestUtils.t.sol index cac977ce5c..df2fb8b617 100644 --- a/target_chains/ethereum/contracts/forge-test/utils/PythTestUtils.t.sol +++ b/target_chains/ethereum/contracts/forge-test/utils/PythTestUtils.t.sol @@ -15,10 +15,10 @@ import "@pythnetwork/pyth-sdk-solidity/IPyth.sol"; import "@pythnetwork/pyth-sdk-solidity/PythUtils.sol"; import "forge-std/Test.sol"; -import "./WormholeTestUtils.t.sol"; import "./RandTestUtils.t.sol"; +import "./WormholeTestUtils.t.sol"; -abstract contract PythTestUtils is Test, WormholeTestUtils, RandTestUtils { +abstract contract PythTestUtils is Test, RandTestUtils { uint16 constant SOURCE_EMITTER_CHAIN_ID = 0x1; bytes32 constant SOURCE_EMITTER_ADDRESS = 0x71f8dcb863d176e2c420ad6610cf687359612b6fb392e0642b0ca6b1f186aa3b; @@ -28,7 +28,11 @@ abstract contract PythTestUtils is Test, WormholeTestUtils, RandTestUtils { 0x0000000000000000000000000000000000000000000000000000000000000011; uint constant SINGLE_UPDATE_FEE_IN_WEI = 1; - function setUpPyth(address wormhole) public returns (address) { + WormholeTestUtils _wormholeTestUtils; + + function setUpPyth( + WormholeTestUtils wormholeTestUtils + ) public returns (address) { PythUpgradable implementation = new PythUpgradable(); ERC1967Proxy proxy = new ERC1967Proxy( address(implementation), @@ -43,7 +47,7 @@ abstract contract PythTestUtils is Test, WormholeTestUtils, RandTestUtils { emitterAddresses[0] = SOURCE_EMITTER_ADDRESS; pyth.initialize( - wormhole, + wormholeTestUtils.getWormholeReceiverAddr(), emitterChainIds, emitterAddresses, GOVERNANCE_EMITTER_CHAIN_ID, @@ -53,9 +57,33 @@ abstract contract PythTestUtils is Test, WormholeTestUtils, RandTestUtils { SINGLE_UPDATE_FEE_IN_WEI // single update fee in wei ); + _wormholeTestUtils = wormholeTestUtils; + return address(pyth); } + function generateVaa( + uint32 timestamp, + uint16 emitterChainId, + bytes32 emitterAddress, + uint64 sequence, + bytes memory payload, + uint8 numSigners, + bool verifier + ) internal view returns (bytes memory vaa) { + if (!verifier) + return + _wormholeTestUtils.generateVaa( + timestamp, + emitterChainId, + emitterAddress, + sequence, + payload, + numSigners + ); + revert PythErrors.InvalidSigner(); + } + function singleUpdateFeeInWei() public pure returns (uint) { return SINGLE_UPDATE_FEE_IN_WEI; } @@ -159,7 +187,8 @@ abstract contract PythTestUtils is Test, WormholeTestUtils, RandTestUtils { config.source_emitter_address, 0, wormholePayload, - config.numSigners + config.numSigners, + false ); if (config.brokenVaa) { @@ -218,7 +247,8 @@ abstract contract PythTestUtils is Test, WormholeTestUtils, RandTestUtils { config.source_emitter_address, 0, wormholePayload, - config.numSigners + config.numSigners, + false ); if (config.brokenVaa) { @@ -343,7 +373,8 @@ abstract contract PythTestUtils is Test, WormholeTestUtils, RandTestUtils { rootDigest, // this can have bytes past this for future versions futureData ), - numSigners + numSigners, + false ); } @@ -369,7 +400,7 @@ abstract contract PythTestUtils is Test, WormholeTestUtils, RandTestUtils { } } -contract PythUtilsTest is Test, WormholeTestUtils, PythTestUtils, IPythEvents { +contract PythUtilsTest is Test, PythTestUtils, IPythEvents { function testConvertToUnit() public { // Price can't be negative vm.expectRevert(); diff --git a/target_chains/ethereum/contracts/forge-test/utils/WormholeTestUtils.t.sol b/target_chains/ethereum/contracts/forge-test/utils/WormholeTestUtils.t.sol index bdfa6f04f6..afaf284a49 100644 --- a/target_chains/ethereum/contracts/forge-test/utils/WormholeTestUtils.t.sol +++ b/target_chains/ethereum/contracts/forge-test/utils/WormholeTestUtils.t.sol @@ -14,7 +14,7 @@ import "../../contracts/wormhole-receiver/ReceiverGovernanceStructs.sol"; import "forge-std/Test.sol"; -abstract contract WormholeTestUtils is Test { +abstract contract AbstractWormholeTestUtils is Test { uint256[] currentSigners; address wormholeReceiverAddr; uint16 constant CHAIN_ID = 2; // Ethereum @@ -22,36 +22,13 @@ abstract contract WormholeTestUtils is Test { bytes32 constant GOVERNANCE_CONTRACT = 0x0000000000000000000000000000000000000000000000000000000000000004; - function setUpWormhole(uint8 numGuardians) public returns (address) { - Implementation wormholeImpl = new Implementation(); - Setup wormholeSetup = new Setup(); - - Wormhole wormhole = new Wormhole(address(wormholeSetup), new bytes(0)); - - address[] memory initSigners = new address[](numGuardians); - currentSigners = new uint256[](numGuardians); - - for (uint256 i = 0; i < numGuardians; ++i) { - currentSigners[i] = i + 1; - initSigners[i] = vm.addr(currentSigners[i]); // i+1 is the private key for the i-th signer. - } - - // These values are the default values used in our tilt test environment - // and are not important. - Setup(address(wormhole)).setup( - address(wormholeImpl), - initSigners, - CHAIN_ID, // Ethereum chain ID - GOVERNANCE_CHAIN_ID, // Governance source chain ID (1 = solana) - GOVERNANCE_CONTRACT // Governance source address - ); - - return address(wormhole); + function getWormholeReceiverAddr() public view returns (address) { + return wormholeReceiverAddr; } function setUpWormholeReceiver( uint8 numGuardians - ) public returns (address) { + ) internal returns (address) { ReceiverImplementation wormholeReceiverImpl = new ReceiverImplementation(); ReceiverSetup wormholeReceiverSetup = new ReceiverSetup(); @@ -228,7 +205,7 @@ abstract contract WormholeTestUtils is Test { } } -contract WormholeTestUtilsTest is Test, WormholeTestUtils { +contract WormholeTestUtilsTest is AbstractWormholeTestUtils { uint32 constant TEST_VAA_TIMESTAMP = 112; uint16 constant TEST_EMITTER_CHAIN_ID = 7; bytes32 constant TEST_EMITTER_ADDR = @@ -487,3 +464,9 @@ contract WormholeTestUtilsTest is Test, WormholeTestUtils { assertEq(reason, "VM signature invalid"); } } + +contract WormholeTestUtils is AbstractWormholeTestUtils { + constructor(uint8 numGuardians) { + setUpWormholeReceiver(numGuardians); + } +} diff --git a/target_chains/ethereum/sdk/solidity/PythErrors.sol b/target_chains/ethereum/sdk/solidity/PythErrors.sol index 0ca223d75a..93a31e169c 100644 --- a/target_chains/ethereum/sdk/solidity/PythErrors.sol +++ b/target_chains/ethereum/sdk/solidity/PythErrors.sol @@ -52,4 +52,7 @@ library PythErrors { // The verifier address to set in SetVerifierAddress governance is invalid. // Signature: 0xab8af376 error InvalidVerifierAddressToSet(); + // The signer of the message is invalid. + // Signature: 0x815e1d64 + error InvalidSigner(); } diff --git a/target_chains/ethereum/sdk/solidity/abis/PythErrors.json b/target_chains/ethereum/sdk/solidity/abis/PythErrors.json index dbb8a0b5b1..683c21f378 100644 --- a/target_chains/ethereum/sdk/solidity/abis/PythErrors.json +++ b/target_chains/ethereum/sdk/solidity/abis/PythErrors.json @@ -24,6 +24,11 @@ "name": "InvalidGovernanceTarget", "type": "error" }, + { + "inputs": [], + "name": "InvalidSigner", + "type": "error" + }, { "inputs": [], "name": "InvalidTwapUpdateData", From f28985d37b7e9d4535f0f94d24a505ca7f19fd59 Mon Sep 17 00:00:00 2001 From: Danial Mehrjerdi Date: Tue, 17 Jun 2025 20:00:10 +0100 Subject: [PATCH 7/9] Add set verifier to utils --- .../Pyth.WormholeMerkleAccumulator.t.sol | 2 +- .../contracts/forge-test/PythGovernance.t.sol | 11 ++-- .../forge-test/VerificationExperiments.t.sol | 2 +- .../forge-test/utils/PythTestUtils.t.sol | 66 ++++++++++++++++--- .../forge-test/utils/WormholeTestUtils.t.sol | 4 ++ 5 files changed, 71 insertions(+), 14 deletions(-) diff --git a/target_chains/ethereum/contracts/forge-test/Pyth.WormholeMerkleAccumulator.t.sol b/target_chains/ethereum/contracts/forge-test/Pyth.WormholeMerkleAccumulator.t.sol index 261c6d7a1a..85b19e4422 100644 --- a/target_chains/ethereum/contracts/forge-test/Pyth.WormholeMerkleAccumulator.t.sol +++ b/target_chains/ethereum/contracts/forge-test/Pyth.WormholeMerkleAccumulator.t.sol @@ -553,7 +553,7 @@ contract PythWormholeMerkleAccumulatorTest is Test, PythTestUtils { 0, wormholePayload, 1, // num signers - false + Signer.Wormhole ); updateData = new bytes[](1); diff --git a/target_chains/ethereum/contracts/forge-test/PythGovernance.t.sol b/target_chains/ethereum/contracts/forge-test/PythGovernance.t.sol index 859178a53a..394fcab3a5 100644 --- a/target_chains/ethereum/contracts/forge-test/PythGovernance.t.sol +++ b/target_chains/ethereum/contracts/forge-test/PythGovernance.t.sol @@ -33,7 +33,7 @@ import "./utils/WormholeTestUtils.t.sol"; import "./utils/PythTestUtils.t.sol"; import "./utils/RandTestUtils.t.sol"; -contract PythGovernanceTest is Test, PythTestUtils, PythGovernanceInstructions { +contract PythGovernanceTest is Test, PythTestUtils { using BytesLib for bytes; IPyth public pyth; @@ -264,6 +264,9 @@ contract PythGovernanceTest is Test, PythTestUtils, PythGovernanceInstructions { } function testSetVerifierAddress() public { + setVerifier(address(pyth), 3); + address oldVerifier = address(PythGetters(address(pyth)).verifier()); + // Deploy a new verifier contract address newVerifier = new WormholeTestUtils(3) .getWormholeReceiverAddr(); @@ -281,10 +284,10 @@ contract PythGovernanceTest is Test, PythTestUtils, PythGovernanceInstructions { data, TEST_GOVERNANCE_CHAIN_ID, TEST_GOVERNANCE_EMITTER, - 1 + 2 ); - address oldVerifier = address(PythGetters(address(pyth)).verifier()); + oldVerifier = address(PythGetters(address(pyth)).verifier()); vm.expectEmit(true, true, true, true); emit VerifierAddressSet(oldVerifier, newVerifier); @@ -690,7 +693,7 @@ contract PythGovernanceTest is Test, PythTestUtils, PythGovernanceInstructions { sequence, data, numGuardians, - false + Signer.Wormhole ); } diff --git a/target_chains/ethereum/contracts/forge-test/VerificationExperiments.t.sol b/target_chains/ethereum/contracts/forge-test/VerificationExperiments.t.sol index b83ea37e5e..7b52591e0b 100644 --- a/target_chains/ethereum/contracts/forge-test/VerificationExperiments.t.sol +++ b/target_chains/ethereum/contracts/forge-test/VerificationExperiments.t.sol @@ -269,7 +269,7 @@ contract VerificationExperiments is Test, PythTestUtils { sequence, bytes.concat(root), NUM_GUARDIAN_SIGNERS, - false + Signer.Wormhole ); ++sequence; diff --git a/target_chains/ethereum/contracts/forge-test/utils/PythTestUtils.t.sol b/target_chains/ethereum/contracts/forge-test/utils/PythTestUtils.t.sol index df2fb8b617..4af3537eea 100644 --- a/target_chains/ethereum/contracts/forge-test/utils/PythTestUtils.t.sol +++ b/target_chains/ethereum/contracts/forge-test/utils/PythTestUtils.t.sol @@ -5,6 +5,8 @@ pragma solidity ^0.8.0; import "../../contracts/pyth/PythUpgradable.sol"; import "../../contracts/pyth/PythInternalStructs.sol"; import "../../contracts/pyth/PythAccumulator.sol"; +import "../../contracts/pyth/PythGetters.sol"; +import "../../contracts/pyth/PythGovernanceInstructions.sol"; import "../../contracts/libraries/MerkleTree.sol"; @@ -15,10 +17,15 @@ import "@pythnetwork/pyth-sdk-solidity/IPyth.sol"; import "@pythnetwork/pyth-sdk-solidity/PythUtils.sol"; import "forge-std/Test.sol"; +import "forge-std/console.sol"; import "./RandTestUtils.t.sol"; import "./WormholeTestUtils.t.sol"; -abstract contract PythTestUtils is Test, RandTestUtils { +abstract contract PythTestUtils is + Test, + RandTestUtils, + PythGovernanceInstructions +{ uint16 constant SOURCE_EMITTER_CHAIN_ID = 0x1; bytes32 constant SOURCE_EMITTER_ADDRESS = 0x71f8dcb863d176e2c420ad6610cf687359612b6fb392e0642b0ca6b1f186aa3b; @@ -28,7 +35,39 @@ abstract contract PythTestUtils is Test, RandTestUtils { 0x0000000000000000000000000000000000000000000000000000000000000011; uint constant SINGLE_UPDATE_FEE_IN_WEI = 1; + enum Signer { + Wormhole, + Verifier + } + WormholeTestUtils _wormholeTestUtils; + WormholeTestUtils _verifierTestUtils; + + function setVerifier(address pyth, uint8 numSigners) internal { + _verifierTestUtils = new WormholeTestUtils(numSigners); + uint64 sequence = PythGetters(pyth).lastExecutedGovernanceSequence(); + + // Create governance VAA to set new verifier address + bytes memory payload = abi.encodePacked( + MAGIC, + uint8(GovernanceModule.Target), + uint8(GovernanceAction.SetVerifierAddress), + uint16(2), + _verifierTestUtils.getWormholeReceiverAddr() + ); + + bytes memory vaa = generateVaa( + uint32(block.timestamp), + GOVERNANCE_EMITTER_CHAIN_ID, + GOVERNANCE_EMITTER_ADDRESS, + sequence + 1, + payload, + _wormholeTestUtils.getTotalSigners(), + Signer.Wormhole + ); + + PythGovernance(pyth).executeGovernanceInstruction(vaa); + } function setUpPyth( WormholeTestUtils wormholeTestUtils @@ -58,7 +97,6 @@ abstract contract PythTestUtils is Test, RandTestUtils { ); _wormholeTestUtils = wormholeTestUtils; - return address(pyth); } @@ -69,9 +107,9 @@ abstract contract PythTestUtils is Test, RandTestUtils { uint64 sequence, bytes memory payload, uint8 numSigners, - bool verifier + Signer signer ) internal view returns (bytes memory vaa) { - if (!verifier) + if (signer == Signer.Wormhole) { return _wormholeTestUtils.generateVaa( timestamp, @@ -81,6 +119,17 @@ abstract contract PythTestUtils is Test, RandTestUtils { payload, numSigners ); + } else if (signer == Signer.Verifier) { + return + _verifierTestUtils.generateVaa( + timestamp, + emitterChainId, + emitterAddress, + sequence, + payload, + numSigners + ); + } revert PythErrors.InvalidSigner(); } @@ -188,7 +237,7 @@ abstract contract PythTestUtils is Test, RandTestUtils { 0, wormholePayload, config.numSigners, - false + Signer.Wormhole ); if (config.brokenVaa) { @@ -204,7 +253,8 @@ abstract contract PythTestUtils is Test, RandTestUtils { uint32(0x504e4155), // PythAccumulator.ACCUMULATOR_MAGIC uint8(1), // major version uint8(0), // minor version - uint8(0), // trailing header size + uint8(1), // trailing header size + uint8(0), // Signer uint8(PythAccumulator.UpdateType.WormholeMerkle), uint16(wormholeMerkleVaa.length), wormholeMerkleVaa, @@ -248,7 +298,7 @@ abstract contract PythTestUtils is Test, RandTestUtils { 0, wormholePayload, config.numSigners, - false + Signer.Wormhole ); if (config.brokenVaa) { @@ -374,7 +424,7 @@ abstract contract PythTestUtils is Test, RandTestUtils { futureData ), numSigners, - false + Signer.Wormhole ); } diff --git a/target_chains/ethereum/contracts/forge-test/utils/WormholeTestUtils.t.sol b/target_chains/ethereum/contracts/forge-test/utils/WormholeTestUtils.t.sol index afaf284a49..77de95d143 100644 --- a/target_chains/ethereum/contracts/forge-test/utils/WormholeTestUtils.t.sol +++ b/target_chains/ethereum/contracts/forge-test/utils/WormholeTestUtils.t.sol @@ -469,4 +469,8 @@ contract WormholeTestUtils is AbstractWormholeTestUtils { constructor(uint8 numGuardians) { setUpWormholeReceiver(numGuardians); } + + function getTotalSigners() public view returns (uint8) { + return uint8(currentSigners.length); + } } From 77816298781a6193050bfbf66edb045d0f067713 Mon Sep 17 00:00:00 2001 From: Danial Mehrjerdi Date: Tue, 17 Jun 2025 20:38:05 +0100 Subject: [PATCH 8/9] Add test for verifier --- .../Pyth.WormholeMerkleAccumulator.t.sol | 78 ++++++++++++++++++- .../ethereum/contracts/forge-test/Pyth.t.sol | 3 +- .../forge-test/utils/PythTestUtils.t.sol | 30 +++++-- 3 files changed, 101 insertions(+), 10 deletions(-) diff --git a/target_chains/ethereum/contracts/forge-test/Pyth.WormholeMerkleAccumulator.t.sol b/target_chains/ethereum/contracts/forge-test/Pyth.WormholeMerkleAccumulator.t.sol index 85b19e4422..c007217cde 100644 --- a/target_chains/ethereum/contracts/forge-test/Pyth.WormholeMerkleAccumulator.t.sol +++ b/target_chains/ethereum/contracts/forge-test/Pyth.WormholeMerkleAccumulator.t.sol @@ -139,7 +139,8 @@ contract PythWormholeMerkleAccumulatorTest is Test, PythTestUtils { } function createWormholeMerkleUpdateData( - PriceFeedMessage[] memory priceFeedMessages + PriceFeedMessage[] memory priceFeedMessages, + Signer signer ) internal returns (bytes[] memory updateData, uint updateFee) { updateData = new bytes[](1); @@ -150,11 +151,29 @@ contract PythWormholeMerkleAccumulatorTest is Test, PythTestUtils { depth += getRandUint8() % 3; - updateData[0] = generateWhMerkleUpdate(priceFeedMessages, depth, 1); + uint8 numSigners = 1; + if (signer == Signer.Wormhole) + numSigners = _wormholeTestUtils.getTotalSigners(); + if (signer == Signer.Verifier) + numSigners = _verifierTestUtils.getTotalSigners(); + + updateData[0] = generateWhMerkleUpdate( + priceFeedMessages, + depth, + numSigners, + signer + ); updateFee = pyth.getUpdateFee(updateData); } + function createWormholeMerkleUpdateData( + PriceFeedMessage[] memory priceFeedMessages + ) internal returns (bytes[] memory updateData, uint updateFee) { + return + createWormholeMerkleUpdateData(priceFeedMessages, Signer.Wormhole); + } + /// @notice This method creates a forward compatible wormhole update data by using a newer minor version, /// setting a trailing header size and generating additional trailing header data of size `trailingHeaderSize` function createFowardCompatibleWormholeMerkleUpdateData( @@ -1145,4 +1164,59 @@ contract PythWormholeMerkleAccumulatorTest is Test, PythTestUtils { assertPriceFeedMessageStored(priceFeedMessages[i]); } } + + /// Testing update price feeds method using wormhole merkle update type. + function testUpdatePriceFeedWithWormholeMerkleAndVerifierSignerWorks( + uint seed + ) public { + setRandSeed(seed); + setVerifier(address(pyth), 10); + + uint numPriceFeeds = (getRandUint() % 10) + 1; + PriceFeedMessage[] + memory priceFeedMessages = generateRandomPriceFeedMessage( + numPriceFeeds + ); + ( + bytes[] memory updateData, + uint updateFee + ) = createWormholeMerkleUpdateData(priceFeedMessages, Signer.Verifier); + + pyth.updatePriceFeeds{value: updateFee}(updateData); + + for (uint i = 0; i < numPriceFeeds; i++) { + assertPriceFeedMessageStored(priceFeedMessages[i]); + } + + // Update the prices again with the same data should work + pyth.updatePriceFeeds{value: updateFee}(updateData); + + for (uint i = 0; i < numPriceFeeds; i++) { + assertPriceFeedMessageStored(priceFeedMessages[i]); + } + + // Update the prices again with updated data should update the prices + for (uint i = 0; i < numPriceFeeds; i++) { + priceFeedMessages[i].price = getRandInt64(); + priceFeedMessages[i].conf = getRandUint64(); + priceFeedMessages[i].expo = getRandInt32(); + + // Increase the publish time if it is not causing an overflow + if (priceFeedMessages[i].publishTime != type(uint64).max) { + priceFeedMessages[i].publishTime += 1; + } + priceFeedMessages[i].emaPrice = getRandInt64(); + priceFeedMessages[i].emaConf = getRandUint64(); + } + + (updateData, updateFee) = createWormholeMerkleUpdateData( + priceFeedMessages + ); + + pyth.updatePriceFeeds{value: updateFee}(updateData); + + for (uint i = 0; i < numPriceFeeds; i++) { + assertPriceFeedMessageStored(priceFeedMessages[i]); + } + } } diff --git a/target_chains/ethereum/contracts/forge-test/Pyth.t.sol b/target_chains/ethereum/contracts/forge-test/Pyth.t.sol index 431fa9d8e7..b224c1f944 100644 --- a/target_chains/ethereum/contracts/forge-test/Pyth.t.sol +++ b/target_chains/ethereum/contracts/forge-test/Pyth.t.sol @@ -134,7 +134,8 @@ contract PythTest is Test, PythTestUtils { updateData[i / batchSize] = generateWhMerkleUpdateWithSource( batchMessages, - config + config, + Signer.Wormhole ); } diff --git a/target_chains/ethereum/contracts/forge-test/utils/PythTestUtils.t.sol b/target_chains/ethereum/contracts/forge-test/utils/PythTestUtils.t.sol index 4af3537eea..61098beb53 100644 --- a/target_chains/ethereum/contracts/forge-test/utils/PythTestUtils.t.sol +++ b/target_chains/ethereum/contracts/forge-test/utils/PythTestUtils.t.sol @@ -17,7 +17,6 @@ import "@pythnetwork/pyth-sdk-solidity/IPyth.sol"; import "@pythnetwork/pyth-sdk-solidity/PythUtils.sol"; import "forge-std/Test.sol"; -import "forge-std/console.sol"; import "./RandTestUtils.t.sol"; import "./WormholeTestUtils.t.sol"; @@ -213,7 +212,8 @@ abstract contract PythTestUtils is function generateWhMerkleUpdateWithSource( PriceFeedMessage[] memory priceFeedMessages, - MerkleUpdateConfig memory config + MerkleUpdateConfig memory config, + Signer signer ) internal returns (bytes memory whMerkleUpdateData) { bytes[] memory encodedPriceFeedMessages = encodePriceFeedMessages( priceFeedMessages @@ -237,7 +237,7 @@ abstract contract PythTestUtils is 0, wormholePayload, config.numSigners, - Signer.Wormhole + signer ); if (config.brokenVaa) { @@ -249,16 +249,17 @@ abstract contract PythTestUtils is ); } + uint256 priceFeedMessageLength = priceFeedMessages.length; whMerkleUpdateData = abi.encodePacked( uint32(0x504e4155), // PythAccumulator.ACCUMULATOR_MAGIC uint8(1), // major version uint8(0), // minor version uint8(1), // trailing header size - uint8(0), // Signer + uint8(signer), // Signer uint8(PythAccumulator.UpdateType.WormholeMerkle), uint16(wormholeMerkleVaa.length), wormholeMerkleVaa, - uint8(priceFeedMessages.length) + uint8(priceFeedMessageLength) ); for (uint i = 0; i < priceFeedMessages.length; i++) { @@ -334,7 +335,8 @@ abstract contract PythTestUtils is function generateWhMerkleUpdate( PriceFeedMessage[] memory priceFeedMessages, uint8 depth, - uint8 numSigners + uint8 numSigners, + Signer signer ) internal returns (bytes memory whMerkleUpdateData) { whMerkleUpdateData = generateWhMerkleUpdateWithSource( priceFeedMessages, @@ -344,7 +346,21 @@ abstract contract PythTestUtils is SOURCE_EMITTER_CHAIN_ID, SOURCE_EMITTER_ADDRESS, false - ) + ), + signer + ); + } + + function generateWhMerkleUpdate( + PriceFeedMessage[] memory priceFeedMessages, + uint8 depth, + uint8 numSigners + ) internal returns (bytes memory whMerkleUpdateData) { + whMerkleUpdateData = generateWhMerkleUpdate( + priceFeedMessages, + depth, + numSigners, + Signer.Wormhole ); } From 0bc888b10eaa16aff3d929c71b3dd803a273c4c3 Mon Sep 17 00:00:00 2001 From: Danial Mehrjerdi Date: Tue, 17 Jun 2025 20:41:44 +0100 Subject: [PATCH 9/9] Remove unused imports --- .../ethereum/contracts/forge-test/PulseSchedulerGovernance.t.sol | 1 - target_chains/ethereum/contracts/forge-test/Pyth.t.sol | 1 - 2 files changed, 2 deletions(-) diff --git a/target_chains/ethereum/contracts/forge-test/PulseSchedulerGovernance.t.sol b/target_chains/ethereum/contracts/forge-test/PulseSchedulerGovernance.t.sol index 4cd48409be..8b411541f9 100644 --- a/target_chains/ethereum/contracts/forge-test/PulseSchedulerGovernance.t.sol +++ b/target_chains/ethereum/contracts/forge-test/PulseSchedulerGovernance.t.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.0; import "forge-std/Test.sol"; -import "forge-std/console.sol"; import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import "../contracts/pulse/SchedulerUpgradeable.sol"; import "@pythnetwork/pulse-sdk-solidity/SchedulerErrors.sol"; diff --git a/target_chains/ethereum/contracts/forge-test/Pyth.t.sol b/target_chains/ethereum/contracts/forge-test/Pyth.t.sol index b224c1f944..af256e3ed8 100644 --- a/target_chains/ethereum/contracts/forge-test/Pyth.t.sol +++ b/target_chains/ethereum/contracts/forge-test/Pyth.t.sol @@ -11,7 +11,6 @@ import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol"; import "./utils/WormholeTestUtils.t.sol"; import "./utils/PythTestUtils.t.sol"; import "./utils/RandTestUtils.t.sol"; -import "forge-std/console.sol"; contract PythTest is Test, PythTestUtils { IPyth public pyth;