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 b9bd83ba0a..7d59ddd3fd 100644 --- a/target_chains/ethereum/contracts/contracts/pyth/PythAccumulator.sol +++ b/target_chains/ethereum/contracts/contracts/pyth/PythAccumulator.sol @@ -29,24 +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) revert PythErrors.InvalidWormholeVaa(); + } else if (signer == Signer.Verifier) { + if (address(verifier()) == address(0)) + revert PythErrors.InvalidUpdateData(); + (vm, valid, ) = verifier().parseAndVerifyVM(encodedVm); + } else { + revert PythErrors.InvalidSigner(); + } + + if (!valid) { + revert PythErrors.InvalidUpdateData(); } - if (!isValidDataSource(vm.emitterChainId, vm.emitterAddress)) + 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; @@ -90,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; } @@ -112,8 +133,9 @@ abstract contract PythAccumulator is PythGetters, PythSetters, AbstractPyth { } } - function extractWormholeMerkleHeaderDigestAndNumUpdatesAndEncodedAndSlotFromAccumulatorUpdate( + function extractWormholeMerkleHeader( bytes calldata accumulatorUpdate, + Signer signer, uint encodedOffset ) internal @@ -148,7 +170,8 @@ abstract contract PythAccumulator is PythGetters, PythSetters, AbstractPyth { encoded, offset, whProofSize - ) + ), + signer ); offset += whProofSize; @@ -171,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(); @@ -460,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(); @@ -477,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/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..107141dac5 100644 --- a/target_chains/ethereum/contracts/contracts/pyth/PythGovernance.sol +++ b/target_chains/ethereum/contracts/contracts/pyth/PythGovernance.sol @@ -40,6 +40,10 @@ abstract contract PythGovernance is ); event TransactionFeeSet(uint oldFee, uint newFee); event FeeWithdrawn(address targetAddress, uint fee); + event VerifierAddressSet( + address oldVerifierAddress, + address newVerifierAddress + ); function verifyGovernanceVM( bytes memory encodedVM @@ -105,6 +109,8 @@ abstract contract PythGovernance is setTransactionFee(parseSetTransactionFeePayload(gi.payload)); } else if (gi.action == GovernanceAction.WithdrawFee) { withdrawFee(parseWithdrawFeePayload(gi.payload)); + } else if (gi.action == GovernanceAction.SetVerifierAddress) { + setVerifierAddress(parseSetVerifierAddressPayload(gi.payload)); } else { revert PythErrors.InvalidGovernanceMessage(); } @@ -270,4 +276,12 @@ abstract contract PythGovernance is emit FeeWithdrawn(payload.targetAddress, payload.fee); } + + function setVerifierAddress( + SetVerifierAddressPayload memory payload + ) internal { + address oldVerifierAddress = address(verifier()); + setVerifier(payload.newVerifierAddress); + 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/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/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.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..c007217cde 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( @@ -143,7 +139,8 @@ contract PythWormholeMerkleAccumulatorTest is } function createWormholeMerkleUpdateData( - PriceFeedMessage[] memory priceFeedMessages + PriceFeedMessage[] memory priceFeedMessages, + Signer signer ) internal returns (bytes[] memory updateData, uint updateFee) { updateData = new bytes[](1); @@ -154,11 +151,29 @@ contract PythWormholeMerkleAccumulatorTest is 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( @@ -530,15 +545,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 +561,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 + Signer.Wormhole ); 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 +593,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 ); } @@ -1142,4 +1164,59 @@ contract PythWormholeMerkleAccumulatorTest is 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 e7b41accff..af256e3ed8 100644 --- a/target_chains/ethereum/contracts/forge-test/Pyth.t.sol +++ b/target_chains/ethereum/contracts/forge-test/Pyth.t.sol @@ -11,9 +11,8 @@ 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, 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 +31,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)); @@ -134,7 +133,8 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils { updateData[i / batchSize] = generateWhMerkleUpdateWithSource( batchMessages, - config + config, + Signer.Wormhole ); } diff --git a/target_chains/ethereum/contracts/forge-test/PythGovernance.t.sol b/target_chains/ethereum/contracts/forge-test/PythGovernance.t.sol index 148f507088..394fcab3a5 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 { 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( @@ -267,6 +263,38 @@ contract PythGovernanceTest is assertEq(address(PythGetters(address(pyth)).wormhole()), newWormhole); } + 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(); + + // 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, + 2 + ); + + 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; @@ -664,7 +692,8 @@ contract PythGovernanceTest is emitterAddress, sequence, data, - numGuardians + numGuardians, + Signer.Wormhole ); } @@ -706,4 +735,8 @@ contract PythGovernanceTest is ); event TransactionFeeSet(uint oldFee, uint newFee); event FeeWithdrawn(address recipient, uint256 fee); + event VerifierAddressSet( + address oldVerifierAddress, + address newVerifierAddress + ); } diff --git a/target_chains/ethereum/contracts/forge-test/VerificationExperiments.t.sol b/target_chains/ethereum/contracts/forge-test/VerificationExperiments.t.sol index eb9e852298..7b52591e0b 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, + 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 cac977ce5c..61098beb53 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,14 @@ 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, + PythGovernanceInstructions +{ uint16 constant SOURCE_EMITTER_CHAIN_ID = 0x1; bytes32 constant SOURCE_EMITTER_ADDRESS = 0x71f8dcb863d176e2c420ad6610cf687359612b6fb392e0642b0ca6b1f186aa3b; @@ -28,7 +34,43 @@ abstract contract PythTestUtils is Test, WormholeTestUtils, RandTestUtils { 0x0000000000000000000000000000000000000000000000000000000000000011; uint constant SINGLE_UPDATE_FEE_IN_WEI = 1; - function setUpPyth(address wormhole) public returns (address) { + 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 + ) public returns (address) { PythUpgradable implementation = new PythUpgradable(); ERC1967Proxy proxy = new ERC1967Proxy( address(implementation), @@ -43,7 +85,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 +95,43 @@ 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, + Signer signer + ) internal view returns (bytes memory vaa) { + if (signer == Signer.Wormhole) { + return + _wormholeTestUtils.generateVaa( + timestamp, + emitterChainId, + emitterAddress, + sequence, + payload, + numSigners + ); + } else if (signer == Signer.Verifier) { + return + _verifierTestUtils.generateVaa( + timestamp, + emitterChainId, + emitterAddress, + sequence, + payload, + numSigners + ); + } + revert PythErrors.InvalidSigner(); + } + function singleUpdateFeeInWei() public pure returns (uint) { return SINGLE_UPDATE_FEE_IN_WEI; } @@ -136,7 +212,8 @@ abstract contract PythTestUtils is Test, WormholeTestUtils, RandTestUtils { function generateWhMerkleUpdateWithSource( PriceFeedMessage[] memory priceFeedMessages, - MerkleUpdateConfig memory config + MerkleUpdateConfig memory config, + Signer signer ) internal returns (bytes memory whMerkleUpdateData) { bytes[] memory encodedPriceFeedMessages = encodePriceFeedMessages( priceFeedMessages @@ -159,7 +236,8 @@ abstract contract PythTestUtils is Test, WormholeTestUtils, RandTestUtils { config.source_emitter_address, 0, wormholePayload, - config.numSigners + config.numSigners, + signer ); if (config.brokenVaa) { @@ -171,15 +249,17 @@ abstract contract PythTestUtils is Test, WormholeTestUtils, RandTestUtils { ); } + uint256 priceFeedMessageLength = priceFeedMessages.length; whMerkleUpdateData = abi.encodePacked( uint32(0x504e4155), // PythAccumulator.ACCUMULATOR_MAGIC uint8(1), // major version uint8(0), // minor version - uint8(0), // trailing header size + uint8(1), // trailing header size + uint8(signer), // Signer uint8(PythAccumulator.UpdateType.WormholeMerkle), uint16(wormholeMerkleVaa.length), wormholeMerkleVaa, - uint8(priceFeedMessages.length) + uint8(priceFeedMessageLength) ); for (uint i = 0; i < priceFeedMessages.length; i++) { @@ -218,7 +298,8 @@ abstract contract PythTestUtils is Test, WormholeTestUtils, RandTestUtils { config.source_emitter_address, 0, wormholePayload, - config.numSigners + config.numSigners, + Signer.Wormhole ); if (config.brokenVaa) { @@ -254,7 +335,8 @@ abstract contract PythTestUtils is Test, WormholeTestUtils, RandTestUtils { function generateWhMerkleUpdate( PriceFeedMessage[] memory priceFeedMessages, uint8 depth, - uint8 numSigners + uint8 numSigners, + Signer signer ) internal returns (bytes memory whMerkleUpdateData) { whMerkleUpdateData = generateWhMerkleUpdateWithSource( priceFeedMessages, @@ -264,7 +346,21 @@ abstract contract PythTestUtils is Test, WormholeTestUtils, RandTestUtils { 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 ); } @@ -343,7 +439,8 @@ abstract contract PythTestUtils is Test, WormholeTestUtils, RandTestUtils { rootDigest, // this can have bytes past this for future versions futureData ), - numSigners + numSigners, + Signer.Wormhole ); } @@ -369,7 +466,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..77de95d143 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,13 @@ contract WormholeTestUtilsTest is Test, WormholeTestUtils { assertEq(reason, "VM signature invalid"); } } + +contract WormholeTestUtils is AbstractWormholeTestUtils { + constructor(uint8 numGuardians) { + setUpWormholeReceiver(numGuardians); + } + + function getTotalSigners() public view returns (uint8) { + return uint8(currentSigners.length); + } +} diff --git a/target_chains/ethereum/sdk/solidity/PythErrors.sol b/target_chains/ethereum/sdk/solidity/PythErrors.sol index ad98f11e30..93a31e169c 100644 --- a/target_chains/ethereum/sdk/solidity/PythErrors.sol +++ b/target_chains/ethereum/sdk/solidity/PythErrors.sol @@ -49,4 +49,10 @@ 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(); + // 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 def11cb07a..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", @@ -44,6 +49,11 @@ "name": "InvalidUpdateDataSource", "type": "error" }, + { + "inputs": [], + "name": "InvalidVerifierAddressToSet", + "type": "error" + }, { "inputs": [], "name": "InvalidWormholeAddressToSet",