[evm] Wormhole receiver deploy all (#1009)

* Add verification logic for evm set wormhole address instruction

* Minor improvements and cleanup on contract manager evm

* Batch deploy script

* Better docs on verification

* Fix zkSync deployment script and update documentation
This commit is contained in:
Mohammad Amin Khashkhashi Moghaddam 2023-08-09 18:22:47 +02:00 committed by GitHub
parent caca2da9e2
commit e422fb9321
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 252 additions and 72 deletions

View File

@ -5,6 +5,7 @@ import { createHash } from "crypto";
import { DefaultStore } from "../src/store";
import {
CosmosUpgradeContract,
EvmSetWormholeAddress,
EvmUpgradeContract,
getProposalInstructions,
MultisigParser,
@ -17,7 +18,8 @@ import {
} from "@pythnetwork/client/lib/cluster";
import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet";
import { AccountMeta, Keypair, PublicKey } from "@solana/web3.js";
import { EvmContract } from "../src/contracts/evm";
import { EvmContract, WormholeEvmContract } from "../src/contracts/evm";
import Web3 from "web3";
const parser = yargs(hideBin(process.argv))
.scriptName("check_proposal.ts")
@ -55,6 +57,42 @@ async function main() {
for (const instruction of parsedInstructions) {
if (instruction instanceof WormholeMultisigInstruction) {
if (instruction.governanceAction instanceof EvmSetWormholeAddress) {
console.log(
`Verifying EVM set wormhole address on ${instruction.governanceAction.targetChainId}`
);
for (const chain of Object.values(DefaultStore.chains)) {
if (
chain instanceof EvmChain &&
chain.isMainnet() === (cluster === "mainnet-beta") &&
chain.wormholeChainName ===
instruction.governanceAction.targetChainId
) {
const address = instruction.governanceAction.address;
const contract = new WormholeEvmContract(chain, address);
const currentIndex = await contract.getCurrentGuardianSetIndex();
const guardianSet = await contract.getGuardianSet();
const proxyContract = new EvmContract(chain, address);
const proxyCode = await proxyContract.getCode();
const receiverImplementation =
await proxyContract.getImplementationAddress();
const implementationCode = await new EvmContract(
chain,
receiverImplementation
).getCode();
const proxyDigest = Web3.utils.keccak256(proxyCode);
const implementationDigest =
Web3.utils.keccak256(implementationCode);
const guardianSetDigest = Web3.utils.keccak256(
JSON.stringify(guardianSet)
);
console.log(
`Address:\t\t${address}\nproxy digest:\t\t${proxyDigest}\nimplementation digest:\t${implementationDigest} \nguardian set index:\t${currentIndex} \nguardian set:\t\t${guardianSetDigest}`
);
}
}
}
if (instruction.governanceAction instanceof EvmUpgradeContract) {
console.log(
`Verifying EVM Upgrade Contract on ${instruction.governanceAction.targetChainId}`

View File

@ -11,6 +11,7 @@ import {
SetDataSources,
SetValidPeriod,
DataSource,
EvmSetWormholeAddress,
} from "xc_admin_common";
import { AptosClient } from "aptos";
import Web3 from "web3";
@ -40,7 +41,9 @@ export abstract class Chain extends Storable {
super();
this.wormholeChainName = wormholeChainName as ChainName;
if (toChainId(this.wormholeChainName) === undefined)
throw new Error(`Invalid chain name ${wormholeChainName}`);
throw new Error(
`Invalid chain name ${wormholeChainName}. Try rebuilding xc_admin_common package`
);
}
getId(): string {
@ -270,6 +273,10 @@ export class EvmChain extends Chain {
return new EvmUpgradeContract(this.wormholeChainName, address).encode();
}
generateGovernanceSetWormholeAddressPayload(address: string): Buffer {
return new EvmSetWormholeAddress(this.wormholeChainName, address).encode();
}
toJson(): any {
return {
id: this.id,
@ -294,6 +301,47 @@ export class EvmChain extends Chain {
}
return gasPrice;
}
/**
* Deploys a contract on this chain
* @param privateKey hex string of the 32 byte private key without the 0x prefix
* @param abi the abi of the contract, can be obtained from the compiled contract json file
* @param bytecode bytecode of the contract, can be obtained from the compiled contract json file
* @param deployArgs arguments to pass to the constructor
* @returns the address of the deployed contract
*/
async deploy(
privateKey: string,
abi: any,
bytecode: string,
deployArgs: any[]
): Promise<string> {
const web3 = new Web3(this.getRpcUrl());
const signer = web3.eth.accounts.privateKeyToAccount(privateKey);
web3.eth.accounts.wallet.add(signer);
const contract = new web3.eth.Contract(abi);
const deployTx = contract.deploy({ data: bytecode, arguments: deployArgs });
const gas = await deployTx.estimateGas();
const gasPrice = await this.getGasPrice();
const deployerBalance = await web3.eth.getBalance(signer.address);
const gasDiff = BigInt(gas) * BigInt(gasPrice) - BigInt(deployerBalance);
if (gasDiff > 0n) {
throw new Error(
`Insufficient funds to deploy contract. Need ${gas} (gas) * ${gasPrice} (gasPrice)= ${
BigInt(gas) * BigInt(gasPrice)
} wei, but only have ${deployerBalance} wei. We need ${
Number(gasDiff) / 10 ** 18
} ETH more.`
);
}
const deployedContract = await deployTx.send({
from: signer.address,
gas,
gasPrice,
});
return deployedContract.options.address;
}
}
export class AptosChain extends Chain {

View File

@ -303,39 +303,6 @@ export class EvmContract extends Contract {
return result;
}
static async deploy(
chain: EvmChain,
privateKey: string,
abi: any,
bytecode: string
): Promise<EvmContract> {
const web3 = new Web3(chain.getRpcUrl());
const signer = web3.eth.accounts.privateKeyToAccount(privateKey);
web3.eth.accounts.wallet.add(signer);
const contract = new web3.eth.Contract(abi);
const deployTx = contract.deploy({ data: bytecode });
const gas = await deployTx.estimateGas();
const gasPrice = await chain.getGasPrice();
const deployerBalance = await web3.eth.getBalance(signer.address);
const gasDiff = BigInt(gas) * BigInt(gasPrice) - BigInt(deployerBalance);
if (gasDiff > 0n) {
throw new Error(
`Insufficient funds to deploy contract. Need ${gas} (gas) * ${gasPrice} (gasPrice)= ${
BigInt(gas) * BigInt(gasPrice)
} wei, but only have ${deployerBalance} wei. We need ${
Number(gasDiff) / 10 ** 18
} ETH more.`
);
}
const deployedContract = await deployTx.send({
from: signer.address,
gas,
gasPrice,
});
return new EvmContract(chain, deployedContract.options.address);
}
getContract() {
const web3 = new Web3(this.chain.getRpcUrl());
const pythContract = new web3.eth.Contract(EXTENDED_PYTH_ABI, this.address);

View File

@ -30,6 +30,10 @@ export abstract class WormholeContract {
for (let i = currentIndex; i < MAINNET_UPGRADE_VAAS.length; i++) {
const vaa = MAINNET_UPGRADE_VAAS[i];
await this.upgradeGuardianSets(senderPrivateKey, Buffer.from(vaa, "hex"));
// make sure the upgrade is complete before continuing
while ((await this.getCurrentGuardianSetIndex()) <= i) {
await new Promise((resolve) => setTimeout(resolve, 5000));
}
}
}
}

View File

@ -8,20 +8,9 @@ repl.evalCode(
"import { loadHotWallet, Vault } from './src/governance';" +
"import { SuiChain, CosmWasmChain, AptosChain, EvmChain } from './src/chains';" +
"import { SuiContract } from './src/contracts/sui';" +
"import { CosmWasmContract } from './src/contracts/cosmwasm';" +
"import { EvmContract } from './src/contracts/evm';" +
"import { WormholeCosmWasmContract, CosmWasmContract } from './src/contracts/cosmwasm';" +
"import { WormholeEvmContract, EvmContract } from './src/contracts/evm';" +
"import { AptosContract } from './src/contracts/aptos';" +
"import { DefaultStore } from './src/store';" +
"DefaultStore"
);
// import * as repl from 'node:repl';
// import { CosmWasmChain, Chains, ChainContracts } from './entities';
// // import { CHAINS_NETWORK_CONFIG } from './chains-manager/chains';
// const replServer = repl.start('Pyth shell> ')
// // const mnemonic = "salon myth guide analyst umbrella load arm first roast pelican stuff satoshi";
// replServer.context.CosmWasmChain = CosmWasmChain;
// replServer.context.Chains = Chains;
// replServer.context.ChainContracts = ChainContracts;

View File

@ -137,13 +137,15 @@ We include artifacts required for verifying the contract in each release. To cre
```
npx sol-merger contracts/pyth/PythUpgradable.sol
npx sol-merger contracts/pyth/ReceiverImplementation.sol
npx truffle run stdjsonin PythUpgradable
npx truffle run stdjsonin ReceiverImplementation
```
These commands create the files `contracts/pyth/PythUpgradable_merged.sol` and `PythUpgradable-input.json` respectively.
The first file is a flattened version of the contract, and the second file is the standard json input of the contract.
These commands create the files `contracts/pyth/PythUpgradable_merged.sol`, `contracts/pyth/ReceiverImplementation_merged.sol`, `PythUpgradable-input.json`, and `ReceiverImplementation-input.json` respectively.
The `.sol` files are the flattened version of the contracts, and the `.json` files are the standard json input of the contracts.
Please include both of these in the verification folder of the release.
Please include all of these in the release.
## Verifying the contract
@ -168,14 +170,13 @@ compile it to their binary format (zk-solc) and deploy it. As of this writing th
contract or a new contract do the following steps in addition to the steps described above:
1. Update the [`hardhad.config.ts`](./hardhat.config.ts) file.
2. Add the configuration files to `truffle-config.js` and `.env.prod.<network>` file as described above. Truffle
config is required as the above deployment script still works in changing the contract (except upgrades).
2. Add the required chain configuration in the contract manager files as described above.
3. Run `npx hardhat clean && npx hardhat compile` to have a clean compile the contracts.
4. Prepare the enviornment:
- Export the secret recovery phrase for the deployment account. Please store it in a file and read
the file into `MNEMONIC` environment variable like so: `export MNEMONIC=$(cat path/to/mnemonic)`.
- Copy the correct env file (e.g: `.env.production.zksync`) to `.env`.
- Create the env settings by running `node create-env.js zksync` and verifying the `.env` file.
5. If you wish to deploy the contract run `npx hardhat deploy-zksync --network <network> --script deploy/zkSyncDeploy` to deploy it to the new network. Otherwise
run `npx hardhat deploy-zksync --network <network> --script deploy/zkSyncDeployNewPythImpl.ts` to get a new implementation address. Then put it in

View File

@ -6,6 +6,7 @@ import { CHAINS } from "xc_admin_common";
import { assert } from "chai";
import { writeFileSync } from "fs";
const { getDefaultConfig } = require("../scripts/contractManagerConfig");
loadEnv("./");
function envOrErr(name: string): string {
@ -36,9 +37,16 @@ export default async function (hre: HardhatRuntimeEnvironment) {
// await depositHandle.wait();
// Deploy WormholeReceiver contract.
const initialSigners = JSON.parse(envOrErr("INIT_SIGNERS"));
const whGovernanceChainId = envOrErr("INIT_GOV_CHAIN_ID");
const whGovernanceContract = envOrErr("INIT_GOV_CONTRACT"); // bytes32
const {
wormholeGovernanceChainId,
wormholeGovernanceContract,
wormholeInitialSigners,
governanceEmitter,
governanceChainId,
emitterAddresses,
emitterChainIds,
} = getDefaultConfig(envOrErr("MIGRATIONS_NETWORK"));
const chainName = envOrErr("WORMHOLE_CHAIN_NAME");
const wormholeReceiverChainId = CHAINS[chainName];
@ -62,10 +70,10 @@ export default async function (hre: HardhatRuntimeEnvironment) {
"setup",
[
receiverImplContract.address,
initialSigners,
wormholeInitialSigners,
wormholeReceiverChainId,
whGovernanceChainId,
whGovernanceContract,
wormholeGovernanceChainId,
wormholeGovernanceContract,
]
);
@ -79,18 +87,6 @@ export default async function (hre: HardhatRuntimeEnvironment) {
`Deployed WormholeReceiver on ${wormholeReceiverContract.address}`
);
// Deploy Pyth contract.
const emitterChainIds = [
envOrErr("SOLANA_CHAIN_ID"),
envOrErr("PYTHNET_CHAIN_ID"),
];
const emitterAddresses = [
envOrErr("SOLANA_EMITTER"),
envOrErr("PYTHNET_EMITTER"),
];
const governanceChainId = envOrErr("GOVERNANCE_CHAIN_ID");
const governanceEmitter = envOrErr("GOVERNANCE_EMITTER");
// Default value for this field is 0
const governanceInitialSequence = Number(
process.env.GOVERNANCE_INITIAL_SEQUENCE ?? "0"
);

View File

@ -0,0 +1,137 @@
/**
* This script deploys the receiver contracts on all the chains and creates a governance proposal to update the
* wormhole addresses to the deployed receiver contracts.
*/
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import {
DefaultStore,
EvmChain,
loadHotWallet,
WormholeEvmContract,
} from "contract_manager";
import Web3 from "web3";
import { CHAINS } from "xc_admin_common";
import * as fs from "fs";
const { getDefaultConfig } = require("./contractManagerConfig");
const parser = yargs(hideBin(process.argv))
.usage(
"Usage: $0 --contracts <path-to-contract-json-folder> --network <contract_id> --private-key <private-key> --ops-wallet <ops-wallet>"
)
.options({
contract: {
type: "string",
demandOption: true,
desc: "Path to the contract json file containing abi and bytecode",
},
network: {
type: "string",
demandOption: true,
choices: ["testnet", "mainnet"],
desc: "The network to deploy the contract on",
},
"private-key": {
type: "string",
demandOption: true,
desc: "Private key to sign the transactions. Hex format, without 0x prefix.",
},
"ops-wallet": {
type: "string",
demandOption: true,
desc: "Path to operations wallet json file",
},
});
async function memoize(key: string, fn: () => Promise<any>) {
const path = `./cache/${key}.json`;
if (fs.existsSync(path)) {
return JSON.parse(fs.readFileSync(path).toString());
}
const result = await fn();
fs.writeFileSync(path, JSON.stringify(result));
return result;
}
async function main() {
const argv = await parser.argv;
const privateKey = argv["private-key"];
const network = argv["network"];
const setupInfo = require(argv["contract"] + "/ReceiverSetup.json");
const implementationInfo = require(argv["contract"] +
"/ReceiverImplementation.json");
const receiverInfo = require(argv["contract"] + "/WormholeReceiver.json");
const payloads: Buffer[] = [];
for (const chain of Object.values(DefaultStore.chains)) {
if (
chain instanceof EvmChain &&
chain.isMainnet() === (network === "mainnet")
) {
if (chain.wormholeChainName === "zksync") continue; // deploy zksync receiver separately
const {
wormholeGovernanceChainId,
wormholeGovernanceContract,
wormholeInitialSigners,
} = getDefaultConfig(chain.getId());
console.log(chain.getId());
const address = await memoize(chain.getId(), async () => {
const setupAddress = await chain.deploy(
privateKey,
setupInfo.abi,
setupInfo.bytecode,
[]
);
console.log("setupAddress", setupAddress);
const implementationAddress = await chain.deploy(
privateKey,
implementationInfo.abi,
implementationInfo.bytecode,
[]
);
console.log("implementationAddress", implementationAddress);
const web3 = new Web3();
const setup = new web3.eth.Contract(setupInfo.abi, setupAddress);
const initData = setup.methods
.setup(
implementationAddress,
wormholeInitialSigners,
CHAINS[chain.wormholeChainName],
wormholeGovernanceChainId,
wormholeGovernanceContract
)
.encodeABI();
// deploy proxy
const receiverAddress = await chain.deploy(
privateKey,
receiverInfo.abi,
receiverInfo.bytecode,
[setupAddress, initData]
);
const contract = new WormholeEvmContract(chain, receiverAddress);
console.log("receiverAddress", receiverAddress);
await contract.syncMainnetGuardianSets(privateKey);
console.log("synced");
return contract.address;
});
const payload = chain.generateGovernanceSetWormholeAddressPayload(
address.replace("0x", "")
);
payloads.push(payload);
}
}
let vaultName;
if (network === "mainnet") {
vaultName = "mainnet-beta_FVQyHcooAtThJ83XFrNnv74BcinbRH3bRmfFamAHBfuj";
} else {
vaultName = "devnet_6baWtW1zTUVMSJHJQVxDUXWzqrQeYBr6mu31j3bTKwY3";
}
const vault = DefaultStore.vaults[vaultName];
vault.connect(await loadHotWallet(argv["ops-wallet"]));
await vault.proposeWormholeMessage(payloads);
}
main();