From 09fcb158dd45941851b5b66c996d92faf9b8d1b7 Mon Sep 17 00:00:00 2001 From: Ali Behjati Date: Fri, 20 May 2022 13:12:50 +0200 Subject: [PATCH] Add wormhole receiver + docs on how to use it (#216) * Add wormhole receiver + docs on how to use it * Fix a mistake in comment * Update docs + add wormhole receiver address to the registry --- ethereum/.env.test | 5 + ethereum/Deploying.md | 8 +- .../wormhole-receiver/ReceiverGetters.sol | 40 +++++ .../wormhole-receiver/ReceiverGovernance.sol | 95 +++++++++++ .../ReceiverGovernanceStructs.sol | 59 +++++++ .../ReceiverImplementation.sol | 29 ++++ .../wormhole-receiver/ReceiverMessages.sol | 148 ++++++++++++++++++ .../wormhole-receiver/ReceiverSetters.sol | 41 +++++ .../wormhole-receiver/ReceiverSetup.sol | 35 +++++ .../wormhole-receiver/ReceiverState.sol | 47 ++++++ .../wormhole-receiver/ReceiverStructs.sol | 39 +++++ .../wormhole-receiver/WormholeReceiver.sol | 13 ++ .../prod-receiver/1_initial_migration.js | 12 ++ .../2_deploy_wormhole_receiver.js | 38 +++++ .../migrations/prod-receiver/3_deploy_pyth.js | 34 ++++ ethereum/package.json | 1 + .../receiverSubmitGuardianSetUpgrades.js | 46 ++++++ ethereum/test/pyth.js | 1 + 18 files changed, 689 insertions(+), 2 deletions(-) create mode 100644 ethereum/contracts/wormhole-receiver/ReceiverGetters.sol create mode 100644 ethereum/contracts/wormhole-receiver/ReceiverGovernance.sol create mode 100644 ethereum/contracts/wormhole-receiver/ReceiverGovernanceStructs.sol create mode 100644 ethereum/contracts/wormhole-receiver/ReceiverImplementation.sol create mode 100644 ethereum/contracts/wormhole-receiver/ReceiverMessages.sol create mode 100644 ethereum/contracts/wormhole-receiver/ReceiverSetters.sol create mode 100644 ethereum/contracts/wormhole-receiver/ReceiverSetup.sol create mode 100644 ethereum/contracts/wormhole-receiver/ReceiverState.sol create mode 100644 ethereum/contracts/wormhole-receiver/ReceiverStructs.sol create mode 100644 ethereum/contracts/wormhole-receiver/WormholeReceiver.sol create mode 100644 ethereum/migrations/prod-receiver/1_initial_migration.js create mode 100644 ethereum/migrations/prod-receiver/2_deploy_wormhole_receiver.js create mode 100644 ethereum/migrations/prod-receiver/3_deploy_pyth.js create mode 100644 ethereum/scripts/receiverSubmitGuardianSetUpgrades.js diff --git a/ethereum/.env.test b/ethereum/.env.test index 4abcc8e8..1b98543c 100644 --- a/ethereum/.env.test +++ b/ethereum/.env.test @@ -1,5 +1,10 @@ # Migrations Metadata MIGRATIONS_DIR=./migrations/test + +# By default tests are run against the tilt Wormhole deployment. If you wish to test against +# the read-only Wormhole receiver instead, uncomment the following line: +# MIGRATIONS_DIR=./migrations/prod-receiver + MIGRATIONS_NETWORK=development # Wormhole Core Migrations diff --git a/ethereum/Deploying.md b/ethereum/Deploying.md index 79db0e76..49d81efe 100644 --- a/ethereum/Deploying.md +++ b/ethereum/Deploying.md @@ -1,6 +1,6 @@ # Deploying Contracts to Production -Running the Truffle migrations in [`migrations/prod`](migrations/prod) will deploy the contracts to production. +Running the Truffle migrations in [`migrations/prod`](migrations/prod) or [`migrations/prod-receiver`](migrations/prod-receiver/) will deploy the contracts to production. The `prod-receiver` migrations should be used when you need to deploy to a chain that is unsupported by the Wormhole network. The Wormhole Receiver contract acts as a read-only Wormhole endpoint that can verify Wormhole messages even if the Wormhole network has not yet connected the chain. This is the deployment process: @@ -20,6 +20,9 @@ npx apply-registry # Perform the migration npx truffle migrate --network $MIGRATIONS_NETWORK + +# Perform this in first time mainnet deployments with Wormhole Receiver. (Or when guardian sets are upgraded) +npm run receiver-submit-guardian-sets -- --network $MIGRATIONS_NETWORK ``` As a sanity check, it is recommended to deploy the migrations in `migrations/prod` to the Truffle `development` network first. You can do this by using the configuration values in [`.env.prod.development`](.env.prod.development). @@ -29,7 +32,8 @@ As a result of this process for some files (with the network id in their name) i ## `networks` directory Truffle stores the address of the deployed contracts in the build artifacts, which can make local development difficult. We use [`truffle-deploy-registry`](https://github.com/MedXProtocol/truffle-deploy-registry) to store the addresses separately from the artifacts, in the [`networks`](networks) directory. When we need to perform operations on the deployed contracts, such as performing additional migrations, we can run `npx apply-registry` to populate the artifacts with the correct addresses. -Each file in the network directory is named after the network id and contains address of Migration contract and PythUpgradable contract. If you are upgrading the contract it should not change. In case you are deploying to a new network make sure to commit this file. +Each file in the network directory is named after the network id and contains address of Migration contract and PythUpgradable contract +(and Wormhole Receiver if we use `prod-receiver`). If you are upgrading the contract it should not change. In case you are deploying to a new network make sure to commit this file. ## `.openzeppelin` directory In order to handle upgrades safely this directory stores details of the contracts structure, such as implementation addresses diff --git a/ethereum/contracts/wormhole-receiver/ReceiverGetters.sol b/ethereum/contracts/wormhole-receiver/ReceiverGetters.sol new file mode 100644 index 00000000..b7863e08 --- /dev/null +++ b/ethereum/contracts/wormhole-receiver/ReceiverGetters.sol @@ -0,0 +1,40 @@ +// contracts/Getters.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +import "./ReceiverState.sol"; + +contract ReceiverGetters is ReceiverState { + function owner() public view returns (address) { + return _state.owner; + } + + function getGuardianSet(uint32 index) public view returns (ReceiverStructs.GuardianSet memory) { + return _state.guardianSets[index]; + } + + function getCurrentGuardianSetIndex() public view returns (uint32) { + return _state.guardianSetIndex; + } + + function getGuardianSetExpiry() public view returns (uint32) { + return _state.guardianSetExpiry; + } + + function governanceActionIsConsumed(bytes32 hash) public view returns (bool) { + return _state.consumedGovernanceActions[hash]; + } + + function isInitialized(address impl) public view returns (bool) { + return _state.initializedImplementations[impl]; + } + + function governanceChainId() public view returns (uint16){ + return _state.provider.governanceChainId; + } + + function governanceContract() public view returns (bytes32){ + return _state.provider.governanceContract; + } +} diff --git a/ethereum/contracts/wormhole-receiver/ReceiverGovernance.sol b/ethereum/contracts/wormhole-receiver/ReceiverGovernance.sol new file mode 100644 index 00000000..e1fb4fe2 --- /dev/null +++ b/ethereum/contracts/wormhole-receiver/ReceiverGovernance.sol @@ -0,0 +1,95 @@ +// contracts/Governance.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +import "./ReceiverStructs.sol"; +import "./ReceiverGovernanceStructs.sol"; +import "./ReceiverMessages.sol"; +import "./ReceiverSetters.sol"; + +import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Upgrade.sol"; + +abstract contract ReceiverGovernance is ReceiverGovernanceStructs, ReceiverMessages, ReceiverSetters, ERC1967Upgrade { + event ContractUpgraded(address indexed oldContract, address indexed newContract); + event OwnershipTransfered(address indexed oldOwner, address indexed newOwner); + + // "Core" (left padded) + bytes32 constant module = 0x00000000000000000000000000000000000000000000000000000000436f7265; + + function submitNewGuardianSet(bytes memory _vm) public { + ReceiverStructs.VM memory vm = parseVM(_vm); + + (bool isValid, string memory reason) = verifyGovernanceVM(vm); + require(isValid, reason); + + ReceiverGovernanceStructs.GuardianSetUpgrade memory upgrade = parseGuardianSetUpgrade(vm.payload); + + require(upgrade.module == module, "invalid Module"); + + require(upgrade.newGuardianSet.keys.length > 0, "new guardian set is empty"); + require(upgrade.newGuardianSetIndex == getCurrentGuardianSetIndex() + 1, "index must increase in steps of 1"); + + setGovernanceActionConsumed(vm.hash); + + expireGuardianSet(getCurrentGuardianSetIndex()); + storeGuardianSet(upgrade.newGuardianSet, upgrade.newGuardianSetIndex); + updateGuardianSetIndex(upgrade.newGuardianSetIndex); + } + + function upgradeImplementation(address newImplementation) public onlyOwner { + address currentImplementation = _getImplementation(); + + _upgradeTo(newImplementation); + + // Call initialize function of the new implementation + (bool success, bytes memory reason) = newImplementation.delegatecall(abi.encodeWithSignature("initialize()")); + + require(success, string(reason)); + + emit ContractUpgraded(currentImplementation, newImplementation); + } + + function verifyGovernanceVM(ReceiverStructs.VM memory vm) internal view returns (bool, string memory){ + // validate vm + (bool isValid, string memory reason) = verifyVM(vm); + if (!isValid){ + return (false, reason); + } + + // only current guardianset can sign governance packets + if (vm.guardianSetIndex != getCurrentGuardianSetIndex()) { + return (false, "not signed by current guardian set"); + } + + // verify source + if (uint16(vm.emitterChainId) != governanceChainId()) { + return (false, "wrong governance chain"); + } + if (vm.emitterAddress != governanceContract()) { + return (false, "wrong governance contract"); + } + + // prevent re-entry + if (governanceActionIsConsumed(vm.hash)){ + return (false, "governance action already consumed"); + } + + return (true, ""); + } + + function transferOwnership(address newOwner) public onlyOwner { + require(newOwner != address(0), "new owner cannot be the zero address"); + + address currentOwner = owner(); + + setOwner(newOwner); + + emit OwnershipTransfered(currentOwner, newOwner); + } + + modifier onlyOwner() { + require(owner() == msg.sender, "caller is not the owner"); + _; + } +} diff --git a/ethereum/contracts/wormhole-receiver/ReceiverGovernanceStructs.sol b/ethereum/contracts/wormhole-receiver/ReceiverGovernanceStructs.sol new file mode 100644 index 00000000..d0570686 --- /dev/null +++ b/ethereum/contracts/wormhole-receiver/ReceiverGovernanceStructs.sol @@ -0,0 +1,59 @@ +// contracts/GovernanceStructs.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +import "../libraries/external/BytesLib.sol"; +import "./ReceiverStructs.sol"; + +contract ReceiverGovernanceStructs { + using BytesLib for bytes; + + enum GovernanceAction { + UpgradeContract, + UpgradeGuardianset + } + + struct GuardianSetUpgrade { + bytes32 module; + uint8 action; + uint16 chain; + + ReceiverStructs.GuardianSet newGuardianSet; + uint32 newGuardianSetIndex; + } + + function parseGuardianSetUpgrade(bytes memory encodedUpgrade) public pure returns (GuardianSetUpgrade memory gsu) { + uint index = 0; + + gsu.module = encodedUpgrade.toBytes32(index); + index += 32; + + gsu.action = encodedUpgrade.toUint8(index); + index += 1; + + require(gsu.action == 2, "invalid GuardianSetUpgrade"); + + gsu.chain = encodedUpgrade.toUint16(index); + index += 2; + + gsu.newGuardianSetIndex = encodedUpgrade.toUint32(index); + index += 4; + + uint8 guardianLength = encodedUpgrade.toUint8(index); + index += 1; + + gsu.newGuardianSet = ReceiverStructs.GuardianSet({ + keys : new address[](guardianLength), + expirationTime : 0 + }); + + for(uint i = 0; i < guardianLength; i++) { + gsu.newGuardianSet.keys[i] = encodedUpgrade.toAddress(index); + index += 20; + } + + require(encodedUpgrade.length == index, "invalid GuardianSetUpgrade"); + } + +} diff --git a/ethereum/contracts/wormhole-receiver/ReceiverImplementation.sol b/ethereum/contracts/wormhole-receiver/ReceiverImplementation.sol new file mode 100644 index 00000000..283da814 --- /dev/null +++ b/ethereum/contracts/wormhole-receiver/ReceiverImplementation.sol @@ -0,0 +1,29 @@ +// contracts/Implementation.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; +pragma experimental ABIEncoderV2; + +import "./ReceiverGovernance.sol"; + +import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Upgrade.sol"; + +contract ReceiverImplementation is ReceiverGovernance { + + modifier initializer() { + address implementation = ERC1967Upgrade._getImplementation(); + + require( + !isInitialized(implementation), + "already initialized" + ); + + setInitialized(implementation); + + _; + } + + fallback() external payable {revert("unsupported");} + + receive() external payable {revert("the Wormhole Receiver contract does not accept assets");} +} diff --git a/ethereum/contracts/wormhole-receiver/ReceiverMessages.sol b/ethereum/contracts/wormhole-receiver/ReceiverMessages.sol new file mode 100644 index 00000000..33c45094 --- /dev/null +++ b/ethereum/contracts/wormhole-receiver/ReceiverMessages.sol @@ -0,0 +1,148 @@ +// contracts/Messages.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; +pragma experimental ABIEncoderV2; + +import "./ReceiverGetters.sol"; +import "./ReceiverStructs.sol"; +import "../libraries/external/BytesLib.sol"; + + +contract ReceiverMessages is ReceiverGetters { + using BytesLib for bytes; + + /// @dev parseAndVerifyVM serves to parse an encodedVM and wholy validate it for consumption + function parseAndVerifyVM(bytes calldata encodedVM) public view returns (ReceiverStructs.VM memory vm, bool valid, string memory reason) { + vm = parseVM(encodedVM); + (valid, reason) = verifyVM(vm); + } + + /** + * @dev `verifyVM` serves to validate an arbitrary vm against a valid Guardian set + * - it aims to make sure the VM is for a known guardianSet + * - it aims to ensure the guardianSet is not expired + * - it aims to ensure the VM has reached quorum + * - it aims to verify the signatures provided against the guardianSet + */ + function verifyVM(ReceiverStructs.VM memory vm) public view returns (bool valid, string memory reason) { + /// @dev Obtain the current guardianSet for the guardianSetIndex provided + ReceiverStructs.GuardianSet memory guardianSet = getGuardianSet(vm.guardianSetIndex); + + /** + * @dev Checks whether the guardianSet has zero keys + * WARNING: This keys check is critical to ensure the guardianSet has keys present AND to ensure + * that guardianSet key size doesn't fall to zero and negatively impact quorum assessment. If guardianSet + * key length is 0 and vm.signatures length is 0, this could compromise the integrity of both vm and + * signature verification. + */ + if(guardianSet.keys.length == 0){ + return (false, "invalid guardian set"); + } + + /// @dev Checks if VM guardian set index matches the current index (unless the current set is expired). + if(vm.guardianSetIndex != getCurrentGuardianSetIndex() && guardianSet.expirationTime < block.timestamp){ + return (false, "guardian set has expired"); + } + + /** + * @dev We're using a fixed point number transformation with 1 decimal to deal with rounding. + * WARNING: This quorum check is critical to assessing whether we have enough Guardian signatures to validate a VM + * if making any changes to this, obtain additional peer review. If guardianSet key length is 0 and + * vm.signatures length is 0, this could compromise the integrity of both vm and signature verification. + */ + if(((guardianSet.keys.length * 10 / 3) * 2) / 10 + 1 > vm.signatures.length){ + return (false, "no quorum"); + } + + /// @dev Verify the proposed vm.signatures against the guardianSet + (bool signaturesValid, string memory invalidReason) = verifySignatures(vm.hash, vm.signatures, guardianSet); + if(!signaturesValid){ + return (false, invalidReason); + } + + /// If we are here, we've validated the VM is a valid multi-sig that matches the guardianSet. + return (true, ""); + } + + /** + * @dev verifySignatures serves to validate arbitrary sigatures against an arbitrary guardianSet + * - it intentionally does not solve for expectations within guardianSet (you should use verifyVM if you need these protections) + * - it intentioanlly does not solve for quorum (you should use verifyVM if you need these protections) + * - it intentionally returns true when signatures is an empty set (you should use verifyVM if you need these protections) + */ + function verifySignatures(bytes32 hash, ReceiverStructs.Signature[] memory signatures, ReceiverStructs.GuardianSet memory guardianSet) public pure returns (bool valid, string memory reason) { + uint8 lastIndex = 0; + for (uint i = 0; i < signatures.length; i++) { + ReceiverStructs.Signature memory sig = signatures[i]; + + /// Ensure that provided signature indices are ascending only + require(i == 0 || sig.guardianIndex > lastIndex, "signature indices must be ascending"); + lastIndex = sig.guardianIndex; + + /// Check to see if the signer of the signature does not match a specific Guardian key at the provided index + if(ecrecover(hash, sig.v, sig.r, sig.s) != guardianSet.keys[sig.guardianIndex]){ + return (false, "VM signature invalid"); + } + } + + /// If we are here, we've validated that the provided signatures are valid for the provided guardianSet + return (true, ""); + } + + /** + * @dev parseVM serves to parse an encodedVM into a vm struct + * - it intentionally performs no validation functions, it simply parses raw into a struct + */ + function parseVM(bytes memory encodedVM) public pure virtual returns (ReceiverStructs.VM memory vm) { + uint index = 0; + + vm.version = encodedVM.toUint8(index); + index += 1; + require(vm.version == 1, "VM version incompatible"); + + vm.guardianSetIndex = encodedVM.toUint32(index); + index += 4; + + // Parse Signatures + uint256 signersLen = encodedVM.toUint8(index); + index += 1; + vm.signatures = new ReceiverStructs.Signature[](signersLen); + for (uint i = 0; i < signersLen; i++) { + vm.signatures[i].guardianIndex = encodedVM.toUint8(index); + index += 1; + + vm.signatures[i].r = encodedVM.toBytes32(index); + index += 32; + vm.signatures[i].s = encodedVM.toBytes32(index); + index += 32; + vm.signatures[i].v = encodedVM.toUint8(index) + 27; + index += 1; + } + + // Hash the body + bytes memory body = encodedVM.slice(index, encodedVM.length - index); + vm.hash = keccak256(abi.encodePacked(keccak256(body))); + + // Parse the body + vm.timestamp = encodedVM.toUint32(index); + index += 4; + + vm.nonce = encodedVM.toUint32(index); + index += 4; + + vm.emitterChainId = encodedVM.toUint16(index); + index += 2; + + vm.emitterAddress = encodedVM.toBytes32(index); + index += 32; + + vm.sequence = encodedVM.toUint64(index); + index += 8; + + vm.consistencyLevel = encodedVM.toUint8(index); + index += 1; + + vm.payload = encodedVM.slice(index, encodedVM.length - index); + } +} diff --git a/ethereum/contracts/wormhole-receiver/ReceiverSetters.sol b/ethereum/contracts/wormhole-receiver/ReceiverSetters.sol new file mode 100644 index 00000000..f30d6995 --- /dev/null +++ b/ethereum/contracts/wormhole-receiver/ReceiverSetters.sol @@ -0,0 +1,41 @@ +// contracts/Setters.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +import "./ReceiverState.sol"; + +contract ReceiverSetters is ReceiverState { + function setOwner(address owner_) internal { + _state.owner = owner_; + } + + function updateGuardianSetIndex(uint32 newIndex) internal { + _state.guardianSetIndex = newIndex; + } + + function expireGuardianSet(uint32 index) internal { + _state.guardianSets[index].expirationTime = uint32(block.timestamp) + 86400; + } + + function storeGuardianSet(ReceiverStructs.GuardianSet memory set, uint32 index) internal { + _state.guardianSets[index] = set; + } + + function setInitialized(address implementatiom) internal { + _state.initializedImplementations[implementatiom] = true; + } + + function setGovernanceActionConsumed(bytes32 hash) internal { + _state.consumedGovernanceActions[hash] = true; + } + + function setGovernanceChainId(uint16 chainId) internal { + _state.provider.governanceChainId = chainId; + } + + function setGovernanceContract(bytes32 governanceContract) internal { + _state.provider.governanceContract = governanceContract; + } + +} diff --git a/ethereum/contracts/wormhole-receiver/ReceiverSetup.sol b/ethereum/contracts/wormhole-receiver/ReceiverSetup.sol new file mode 100644 index 00000000..d0063e55 --- /dev/null +++ b/ethereum/contracts/wormhole-receiver/ReceiverSetup.sol @@ -0,0 +1,35 @@ +// contracts/Implementation.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; +pragma experimental ABIEncoderV2; + +import "./ReceiverGovernance.sol"; + +import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Upgrade.sol"; + +contract ReceiverSetup is ReceiverSetters, ERC1967Upgrade { + function setup( + address implementation, + address[] memory initialGuardians, + uint16 governanceChainId, + bytes32 governanceContract + ) public { + require(initialGuardians.length > 0, "no guardians specified"); + + setOwner(msg.sender); + + ReceiverStructs.GuardianSet memory initialGuardianSet = ReceiverStructs.GuardianSet({ + keys : initialGuardians, + expirationTime : 0 + }); + + storeGuardianSet(initialGuardianSet, 0); + // initial guardian set index is 0, which is the default value of the storage slot anyways + + setGovernanceChainId(governanceChainId); + setGovernanceContract(governanceContract); + + _upgradeTo(implementation); + } +} diff --git a/ethereum/contracts/wormhole-receiver/ReceiverState.sol b/ethereum/contracts/wormhole-receiver/ReceiverState.sol new file mode 100644 index 00000000..a7b90a99 --- /dev/null +++ b/ethereum/contracts/wormhole-receiver/ReceiverState.sol @@ -0,0 +1,47 @@ +// contracts/State.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +import "./ReceiverStructs.sol"; + +contract ReceiverEvents { + event LogGuardianSetChanged( + uint32 oldGuardianIndex, + uint32 newGuardianIndex + ); + + event LogMessagePublished( + address emitter_address, + uint32 nonce, + bytes payload + ); +} + +contract ReceiverStorage { + struct WormholeState { + ReceiverStructs.Provider provider; + + // contract deployer + address owner; + + // Mapping of guardian_set_index => guardian set + mapping(uint32 => ReceiverStructs.GuardianSet) guardianSets; + + // Current active guardian set index + uint32 guardianSetIndex; + + // Period for which a guardian set stays active after it has been replaced + uint32 guardianSetExpiry; + + // Mapping of consumed governance actions + mapping(bytes32 => bool) consumedGovernanceActions; + + // Mapping of initialized implementations + mapping(address => bool) initializedImplementations; + } +} + +contract ReceiverState { + ReceiverStorage.WormholeState _state; +} diff --git a/ethereum/contracts/wormhole-receiver/ReceiverStructs.sol b/ethereum/contracts/wormhole-receiver/ReceiverStructs.sol new file mode 100644 index 00000000..16aef4cb --- /dev/null +++ b/ethereum/contracts/wormhole-receiver/ReceiverStructs.sol @@ -0,0 +1,39 @@ +// contracts/Structs.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +interface ReceiverStructs { + struct Provider { + uint16 governanceChainId; + bytes32 governanceContract; + } + + struct GuardianSet { + address[] keys; + uint32 expirationTime; + } + + struct Signature { + bytes32 r; + bytes32 s; + uint8 v; + uint8 guardianIndex; + } + + struct VM { + uint8 version; + uint32 timestamp; + uint32 nonce; + uint16 emitterChainId; + bytes32 emitterAddress; + uint64 sequence; + uint8 consistencyLevel; + bytes payload; + + uint32 guardianSetIndex; + Signature[] signatures; + + bytes32 hash; + } +} diff --git a/ethereum/contracts/wormhole-receiver/WormholeReceiver.sol b/ethereum/contracts/wormhole-receiver/WormholeReceiver.sol new file mode 100644 index 00000000..e1f58d38 --- /dev/null +++ b/ethereum/contracts/wormhole-receiver/WormholeReceiver.sol @@ -0,0 +1,13 @@ +// contracts/Wormhole.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +contract WormholeReceiver is ERC1967Proxy { + constructor (address setup, bytes memory initData) ERC1967Proxy( + setup, + initData + ) { } +} diff --git a/ethereum/migrations/prod-receiver/1_initial_migration.js b/ethereum/migrations/prod-receiver/1_initial_migration.js new file mode 100644 index 00000000..ec354c0e --- /dev/null +++ b/ethereum/migrations/prod-receiver/1_initial_migration.js @@ -0,0 +1,12 @@ +const Migrations = artifacts.require("Migrations"); + +const tdr = require('truffle-deploy-registry'); + +module.exports = async function (deployer, network) { + await deployer.deploy(Migrations); + let migrationsInstance = await Migrations.deployed(); + + if (!tdr.isDryRunNetworkName(network)) { + await tdr.appendInstance(migrationsInstance); + } +}; diff --git a/ethereum/migrations/prod-receiver/2_deploy_wormhole_receiver.js b/ethereum/migrations/prod-receiver/2_deploy_wormhole_receiver.js new file mode 100644 index 00000000..b79b3639 --- /dev/null +++ b/ethereum/migrations/prod-receiver/2_deploy_wormhole_receiver.js @@ -0,0 +1,38 @@ +require("dotenv").config({ path: "../.env" }); + +const tdr = require('truffle-deploy-registry'); + +const ReceiverSetup = artifacts.require("ReceiverSetup"); +const ReceiverImplementation = artifacts.require("ReceiverImplementation"); +const WormholeReceiver = artifacts.require("WormholeReceiver"); + +// CONFIG +const initialSigners = JSON.parse(process.env.INIT_SIGNERS); +const governanceChainId = process.env.INIT_GOV_CHAIN_ID; +const governanceContract = process.env.INIT_GOV_CONTRACT; // bytes32 + +module.exports = async function (deployer, network) { + // deploy setup + await deployer.deploy(ReceiverSetup); + + // deploy implementation + await deployer.deploy(ReceiverImplementation); + + // encode initialisation data + const setup = new web3.eth.Contract(ReceiverSetup.abi, ReceiverSetup.address); + const initData = setup.methods + .setup( + ReceiverImplementation.address, + initialSigners, + governanceChainId, + governanceContract + ) + .encodeABI(); + + // deploy proxy + const wormholeReceiver = await deployer.deploy(WormholeReceiver, ReceiverSetup.address, initData); + + if (!tdr.isDryRunNetworkName(network)) { + await tdr.appendInstance(wormholeReceiver); + } +}; diff --git a/ethereum/migrations/prod-receiver/3_deploy_pyth.js b/ethereum/migrations/prod-receiver/3_deploy_pyth.js new file mode 100644 index 00000000..8b82af64 --- /dev/null +++ b/ethereum/migrations/prod-receiver/3_deploy_pyth.js @@ -0,0 +1,34 @@ +require('dotenv').config({ path: "../.env" }); +const bs58 = require("bs58"); + +const PythUpgradable = artifacts.require("PythUpgradable"); +const WormholeReceiver = artifacts.require("WormholeReceiver"); + +const pyth2WormholeChainId = process.env.PYTH_TO_WORMHOLE_CHAIN_ID; +const pyth2WormholeEmitter = process.env.PYTH_TO_WORMHOLE_EMITTER; + +const { deployProxy } = require("@openzeppelin/truffle-upgrades"); +const tdr = require('truffle-deploy-registry'); + +console.log("pyth2WormholeEmitter: " + pyth2WormholeEmitter) +console.log("pyth2WormholeChainId: " + pyth2WormholeChainId) + +module.exports = async function (deployer, network) { + // Deploy the proxy. This will return an instance of PythUpgradable, + // with the address field corresponding to the fronting ERC1967Proxy. + let proxyInstance = await deployProxy(PythUpgradable, + [ + (await WormholeReceiver.deployed()).address, + pyth2WormholeChainId, + pyth2WormholeEmitter + ], + { deployer }); + + // Add the ERC1967Proxy address to the PythUpgradable contract's + // entry in the registry. This allows us to call upgradeProxy + // functions with the value of PythUpgradable.deployed().address: + // e.g. upgradeProxy(PythUpgradable.deployed().address, NewImplementation) + if (!tdr.isDryRunNetworkName(network)) { + await tdr.appendInstance(proxyInstance); + } +}; diff --git a/ethereum/package.json b/ethereum/package.json index 1babb96f..8e90a0fe 100644 --- a/ethereum/package.json +++ b/ethereum/package.json @@ -23,6 +23,7 @@ "test": "truffle test", "migrate": "mkdir -p build/contracts && cp node_modules/@openzeppelin/contracts/build/contracts/* build/contracts/ && truffle migrate", "flatten": "mkdir -p node_modules/@poanet/solidity-flattener/contracts && cp -r contracts/* node_modules/@poanet/solidity-flattener/contracts/ && poa-solidity-flattener", + "receiver-submit-guardian-sets": "truffle exec scripts/receiverSubmitGuardianSetUpgrades.js", "verify": "patch -u -f node_modules/truffle-plugin-verify/constants.js -i truffle-verify-constants.patch; truffle run verify $npm_config_module@$npm_config_contract_address --network $npm_config_network" }, "author": "", diff --git a/ethereum/scripts/receiverSubmitGuardianSetUpgrades.js b/ethereum/scripts/receiverSubmitGuardianSetUpgrades.js new file mode 100644 index 00000000..686f33d0 --- /dev/null +++ b/ethereum/scripts/receiverSubmitGuardianSetUpgrades.js @@ -0,0 +1,46 @@ +// run this script with truffle exec + +const jsonfile = require("jsonfile"); +const WormholeReceiver = artifacts.require("WormholeReceiver"); +const WormholeReceiverImplementationFullABI = jsonfile.readFileSync( + "../build/contracts/wormhole-receiver/ReceiverImplementation.json" +).abi; + +const GUARDIAN_SET_UPGRADE_1_VAA = + "010000000001007ac31b282c2aeeeb37f3385ee0de5f8e421d30b9e5ae8ba3d4375c1c77a86e77159bb697d9c456d6f8c02d22a94b1279b65b0d6a9957e7d3857423845ac758e300610ac1d2000000030001000000000000000000000000000000000000000000000000000000000000000400000000000005390000000000000000000000000000000000000000000000000000000000436f7265020000000000011358cc3ae5c097b213ce3c81979e1b9f9570746aa5ff6cb952589bde862c25ef4392132fb9d4a42157114de8460193bdf3a2fcf81f86a09765f4762fd1107a0086b32d7a0977926a205131d8731d39cbeb8c82b2fd82faed2711d59af0f2499d16e726f6b211b39756c042441be6d8650b69b54ebe715e234354ce5b4d348fb74b958e8966e2ec3dbd4958a7cdeb5f7389fa26941519f0863349c223b73a6ddee774a3bf913953d695260d88bc1aa25a4eee363ef0000ac0076727b35fbea2dac28fee5ccb0fea768eaf45ced136b9d9e24903464ae889f5c8a723fc14f93124b7c738843cbb89e864c862c38cddcccf95d2cc37a4dc036a8d232b48f62cdd4731412f4890da798f6896a3331f64b48c12d1d57fd9cbe7081171aa1be1d36cafe3867910f99c09e347899c19c38192b6e7387ccd768277c17dab1b7a5027c0b3cf178e21ad2e77ae06711549cfbb1f9c7a9d8096e85e1487f35515d02a92753504a8d75471b9f49edb6fbebc898f403e4773e95feb15e80c9a99c8348d"; +const GUARDIAN_SET_UPGRADE_2_VAA = + "01000000010d0012e6b39c6da90c5dfd3c228edbb78c7a4c97c488ff8a346d161a91db067e51d638c17216f368aa9bdf4836b8645a98018ca67d2fec87d769cabfdf2406bf790a0002ef42b288091a670ef3556596f4f47323717882881eaf38e03345078d07a156f312b785b64dae6e9a87e3d32872f59cb1931f728cecf511762981baf48303668f0103cef2616b84c4e511ff03329e0853f1bd7ee9ac5ba71d70a4d76108bddf94f69c2a8a84e4ee94065e8003c334e899184943634e12043d0dda78d93996da073d190104e76d166b9dac98f602107cc4b44ac82868faf00b63df7d24f177aa391e050902413b71046434e67c770b19aecdf7fce1d1435ea0be7262e3e4c18f50ddc8175c0105d9450e8216d741e0206a50f93b750a47e0a258b80eb8fed1314cc300b3d905092de25cd36d366097b7103ae2d184121329ba3aa2d7c6cc53273f11af14798110010687477c8deec89d36a23e7948feb074df95362fc8dcbd8ae910ac556a1dee1e755c56b9db5d710c940938ed79bc1895a3646523a58bc55f475a23435a373ecfdd0107fb06734864f79def4e192497362513171530daea81f07fbb9f698afe7e66c6d44db21323144f2657d4a5386a954bb94eef9f64148c33aef6e477eafa2c5c984c01088769e82216310d1827d9bd48645ec23e90de4ef8a8de99e2d351d1df318608566248d80cdc83bdcac382b3c30c670352be87f9069aab5037d0b747208eae9c650109e9796497ff9106d0d1c62e184d83716282870cef61a1ee13d6fc485b521adcce255c96f7d1bca8d8e7e7d454b65783a830bddc9d94092091a268d311ecd84c26010c468c9fb6d41026841ff9f8d7368fa309d4dbea3ea4bbd2feccf94a92cc8a20a226338a8e2126cd16f70eaf15b4fc9be2c3fa19def14e071956a605e9d1ac4162010e23fcb6bd445b7c25afb722250c1acbc061ed964ba9de1326609ae012acdfb96942b2a102a2de99ab96327859a34a2b49a767dbdb62e0a1fb26af60fe44fd496a00106bb0bac77ac68b347645f2fb1ad789ea9bd76fb9b2324f25ae06f97e65246f142df717f662e73948317182c62ce87d79c73def0dba12e5242dfc038382812cfe00126da03c5e56cb15aeeceadc1e17a45753ab4dc0ec7bf6a75ca03143ed4a294f6f61bc3f478a457833e43084ecd7c985bf2f55a55f168aac0e030fc49e845e497101626e9d9a5d9e343f00010000000000000000000000000000000000000000000000000000000000000004c1759167c43f501c2000000000000000000000000000000000000000000000000000000000436f7265020000000000021358cc3ae5c097b213ce3c81979e1b9f9570746aa5ff6cb952589bde862c25ef4392132fb9d4a42157114de8460193bdf3a2fcf81f86a09765f4762fd1107a0086b32d7a0977926a205131d8731d39cbeb8c82b2fd82faed2711d59af0f2499d16e726f6b211b39756c042441be6d8650b69b54ebe715e234354ce5b4d348fb74b958e8966e2ec3dbd4958a7cd66b9590e1c41e0b226937bf9217d1d67fd4e91f574a3bf913953d695260d88bc1aa25a4eee363ef0000ac0076727b35fbea2dac28fee5ccb0fea768eaf45ced136b9d9e24903464ae889f5c8a723fc14f93124b7c738843cbb89e864c862c38cddcccf95d2cc37a4dc036a8d232b48f62cdd4731412f4890da798f6896a3331f64b48c12d1d57fd9cbe7081171aa1be1d36cafe3867910f99c09e347899c19c38192b6e7387ccd768277c17dab1b7a5027c0b3cf178e21ad2e77ae06711549cfbb1f9c7a9d8096e85e1487f35515d02a92753504a8d75471b9f49edb6fbebc898f403e4773e95feb15e80c9a99c8348d"; +// const TEST_VAA = +// "01000000020d005f7c6d5d57806e39e2b72f1b35e105b560dcbaa53ca159713897f666bbcca9566a3153bec04131423d31b3c612b0036711a8f3e092d382ee33666310ce9c13f00001f2bb445b90ce41374692d79037ae2fc76d45de890328404ccde3137a244774ca23cc0f74a3b4e89739cdc78a21e7605ec7f2e082e849d74ea284729916e430f40102fac6f17962e6225becdc69d4f3dbef29f7eda52cf189c3cbdec4d1fad98ba63e05aa8d446bd348fbf3dfeeb1753f857421f4d9b47f10a5eccb8927a289fa2e200103a5f7768647a609d20aaf90e09370f7261e2055b6eaded0941d8222a01d2618c11ef5912d8c00f571dd63157579a8ab39584186d5c6995d70ca255ee97d3f9b390104d529bd9ae735d480822cc094cbc74fe66010d233d81bf84278f0b439bc98df956361dd85b8a8ccb2f55bf94606ffb1dfc2260499c25c1027f51a5f7e7d4240ba010584ba8ebace2a0f39e4ca01c0d3f5b8686d18652cc0cd0f6516ce20ded88c796e002231f1198b7501839eb7fe442db09745d34d58c8a8f107a34dc50e19312eea0106744ce85d12622933bb7ffececee1d7eb27a1460f8a2062c2b39fe1524baebe9d2c543cdb9a762ef233fb3fe874f810ae0457ed1be3b087096d377feab781c44e010c972bab8988fb8df3864f0946771ad80affe3d46a9fc8a1ac5377cb14137fb9ec4e6f61e0deffe103cb090dde734edd72885c84023b2ed10d81a1edddfb13d9f7000db6bfe9f7a0a0c9088b9fc5ead7520af1e22dc58034e46d6a90e75a3dc4f9eb4026940bc9b0ce421cf1b3ea61f5e1863b7075e0c0baeeb9bc5793173e9777f6ac000e452480aa2500b30bd3dbc87f3f9f78b6b0c221ec0343db3bbc22833798ff1d8a480dbc9ae10960623c29373d0ac48f42ec33de03c935019f5fc73bc02b95b7b2010f252ddc2ffaecf009f77ce9e57844211723f13b2300c5791114a319943b3ed6c23f54f121061347973a0b23a8a40e8351a7ce848bd0d24581a1d763032e70062e001090ee9b3e84ea1eac58c043a683aa3c1e8b47a94bda3c1db4109e8d92e3f0f2f50562ef7cca4a90008970ec1e0975c9cefc5bd506d823c0d5f2bad70fb582bd9f011110515f473681be0b8457a9c930736520c62687c393f9f12ddc5daa858951ffee5b569a863561d85e5d76f3b0d0c7febdba8272563383ef2fb48cd23c2a856287006279c15f708b000000050000000000000000000000005a58505a96d1dbf8df91cb21b54419fc36e93fde000000000000aff20f010000000000000000000000000000000000000000000000000000000a6830e3a20000000000000000000000000d500b1d8e8ef31e21c99d1db9a6444d3adf1270000500000000000000000000000031adfc3e96825ecb4e6a6bc09349b4a1a9080c1300060000000000000000000000000000000000000000000000000000000000000000"; + +module.exports = async function (callback) { + try { + const accounts = await web3.eth.getAccounts(); + const initialized = new web3.eth.Contract( + WormholeReceiverImplementationFullABI, + WormholeReceiver.address + ); + // Upgrade set 0 to set 1 + await initialized.methods + .submitNewGuardianSet("0x" + GUARDIAN_SET_UPGRADE_1_VAA) + .send({ + value: 0, + from: accounts[0], + gasLimit: 2000000, + }); + // Upgrade set 1 to set 2 + await initialized.methods + .submitNewGuardianSet("0x" + GUARDIAN_SET_UPGRADE_2_VAA) + .send({ + value: 0, + from: accounts[0], + gasLimit: 2000000, + }); + // console.log( + // await initialized.methods.parseAndVerifyVM("0x" + TEST_VAA).call() + // ); + callback(); + } catch (e) { + callback(e); + } +}; diff --git a/ethereum/test/pyth.js b/ethereum/test/pyth.js index 48158ce8..edad2ae5 100644 --- a/ethereum/test/pyth.js +++ b/ethereum/test/pyth.js @@ -7,6 +7,7 @@ const PythStructs = artifacts.require("PythStructs"); const { deployProxy, upgradeProxy } = require("@openzeppelin/truffle-upgrades"); const { expectRevert } = require("@openzeppelin/test-helpers"); +// Use "WormholeReceiver" if you are testing with Wormhole Receiver const Wormhole = artifacts.require("Wormhole"); const PythUpgradable = artifacts.require("PythUpgradable");