| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420 |
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
- import { join } from "node:path";
- import Web3 from "web3";
- import { Contract } from "web3-eth-contract";
- import { InferredOptionType } from "yargs";
- import { PrivateKey, getDefaultDeploymentConfig } from "../src/core/base";
- import { EvmChain } from "../src/core/chains";
- import {
- EvmEntropyContract,
- EvmExecutorContract,
- EvmWormholeContract,
- } from "../src/core/contracts";
- import { DefaultStore } from "../src/node/utils/store";
- export type BaseDeployConfig = {
- gasMultiplier: number;
- gasPriceMultiplier: number;
- jsonOutputDir: string;
- privateKey: PrivateKey;
- }
- // Deploys a contract if it was not deployed before.
- // It will check for the past deployments in file `cacheFile` against a key
- // If `cacheKey` is provided it will be used as the key, else it will use
- // a key - `${chain.getId()}-${artifactName}`
- export async function deployIfNotCached(
- cacheFile: string,
- chain: EvmChain,
- config: BaseDeployConfig,
- artifactName: string,
- deployArgs: any[], // eslint-disable-line @typescript-eslint/no-explicit-any
- cacheKey?: string,
- ): Promise<string> {
- const runIfNotCached = makeCacheFunction(cacheFile);
- const key = cacheKey ?? `${chain.getId()}-${artifactName}`;
- return runIfNotCached(key, async () => {
- const artifact = JSON.parse(
- readFileSync(
- join(
- config.jsonOutputDir,
- `${artifactName}.sol`,
- `${artifactName}.json`,
- ),
- "utf8",
- ),
- );
- // Handle bytecode which can be either a string or an object with an 'object' property
- let bytecode = artifact.bytecode;
- if (
- typeof bytecode === "object" &&
- bytecode !== null &&
- "object" in bytecode
- ) {
- bytecode = bytecode.object;
- }
- // Ensure bytecode starts with 0x
- if (!bytecode.startsWith("0x")) {
- bytecode = `0x${bytecode}`;
- }
- console.log(`Deploying ${artifactName} on ${chain.getId()}...`);
- const addr = await chain.deploy(
- config.privateKey,
- artifact.abi,
- bytecode,
- deployArgs,
- config.gasMultiplier,
- config.gasPriceMultiplier,
- );
- console.log(`✅ Deployed ${artifactName} on ${chain.getId()} at ${addr}`);
- return addr;
- });
- }
- export function getWeb3Contract(
- jsonOutputDir: string,
- artifactName: string,
- address: string,
- ): Contract {
- const artifact = JSON.parse(
- readFileSync(
- join(jsonOutputDir, `${artifactName}.sol`, `${artifactName}.json`),
- "utf8",
- ),
- );
- const web3 = new Web3();
- return new web3.eth.Contract(artifact.abi, address);
- }
- export const COMMON_DEPLOY_OPTIONS = {
- "std-output-dir": {
- type: "string",
- demandOption: true,
- desc: "Path to the Foundry output directory (contains Contract.sol/Contract.json structure)",
- },
- "private-key": {
- type: "string",
- demandOption: true,
- desc: "Private key to sign the transactions with",
- },
- chain: {
- type: "array",
- string: true,
- demandOption: true,
- desc: "Chains to upload the contract on. Must be one of the chains available in the store",
- },
- "deployment-type": {
- type: "string",
- demandOption: false,
- default: "stable",
- desc: "Deployment type to use. Can be 'stable' or 'beta'",
- },
- "gas-multiplier": {
- type: "number",
- demandOption: false,
- // Proxy (ERC1967) contract gas estimate is insufficient in many networks and thus we use 2 by default to make it work.
- default: 2,
- desc: "Gas multiplier to use for the deployment. This is useful when gas estimates are not accurate",
- },
- "gas-price-multiplier": {
- type: "number",
- demandOption: false,
- default: 1,
- desc: "Gas price multiplier to use for the deployment. This is useful when gas price estimates are not accurate",
- },
- "save-contract": {
- type: "boolean",
- demandOption: false,
- default: true,
- desc: "Save the contract to the store",
- },
- } as const;
- export const CHAIN_SELECTION_OPTIONS = {
- testnet: {
- type: "boolean",
- default: false,
- desc: "Upgrade testnet contracts instead of mainnet",
- },
- "all-chains": {
- type: "boolean",
- default: false,
- desc: "Upgrade the contract on all chains. Use with --testnet flag to upgrade all testnet contracts",
- },
- chain: {
- type: "array",
- string: true,
- desc: "Chains to upgrade the contract on",
- },
- } as const;
- export const COMMON_UPGRADE_OPTIONS = {
- ...CHAIN_SELECTION_OPTIONS,
- "private-key": COMMON_DEPLOY_OPTIONS["private-key"],
- "ops-key-path": {
- type: "string",
- demandOption: true,
- desc: "Path to the private key of the proposer to use for the operations multisig governance proposal",
- },
- "std-output": {
- type: "string",
- demandOption: true,
- desc: "Path to the standard JSON output of the pyth contract (build artifact)",
- },
- } as const;
- export function makeCacheFunction(
- cacheFile: string,
- ): (cacheKey: string, fn: () => Promise<string>) => Promise<string> {
- async function runIfNotCached(
- cacheKey: string,
- fn: () => Promise<string>,
- ): Promise<string> {
- const cache = existsSync(cacheFile)
- ? JSON.parse(readFileSync(cacheFile, "utf8"))
- : {};
- if (cache[cacheKey]) {
- return cache[cacheKey];
- }
- const result = await fn();
- cache[cacheKey] = result;
- writeFileSync(cacheFile, JSON.stringify(cache, null, 2));
- return result;
- }
- return runIfNotCached;
- }
- export function getSelectedChains(argv: {
- chain: InferredOptionType<(typeof COMMON_UPGRADE_OPTIONS)["chain"]>;
- testnet: InferredOptionType<(typeof COMMON_UPGRADE_OPTIONS)["testnet"]>;
- allChains: InferredOptionType<(typeof COMMON_UPGRADE_OPTIONS)["all-chains"]>;
- }) {
- const selectedChains: EvmChain[] = [];
- if (argv.allChains && argv.chain)
- throw new Error("Cannot use both --all-chains and --chain");
- if (!argv.allChains && !argv.chain)
- throw new Error("Must use either --all-chains or --chain");
- for (const chain of Object.values(DefaultStore.chains)) {
- if (!(chain instanceof EvmChain)) continue;
- if (
- (argv.allChains && chain.isMainnet() !== argv.testnet) ||
- argv.chain?.includes(chain.getId())
- )
- selectedChains.push(chain);
- }
- if (argv.chain && selectedChains.length !== argv.chain.length)
- throw new Error(
- `Some chains were not found ${selectedChains
- .map((chain) => chain.getId())
- .toString()}`,
- );
- for (const chain of selectedChains) {
- if (chain.isMainnet() != selectedChains[0].isMainnet())
- throw new Error("All chains must be either mainnet or testnet");
- }
- return selectedChains;
- }
- /**
- * Finds the entropy contract for a given EVM chain.
- * @param {EvmChain} chain The EVM chain to find the entropy contract for.
- * @returns The entropy contract for the given EVM chain.
- * @throws {Error} an error if the entropy contract is not found for the given EVM chain.
- */
- export function findEntropyContract(chain: EvmChain): EvmEntropyContract {
- for (const contract of Object.values(DefaultStore.entropy_contracts)) {
- if (contract.getChain().getId() === chain.getId()) {
- return contract;
- }
- }
- throw new Error(`Entropy contract not found for chain ${chain.getId()}`);
- }
- /**
- * Finds the wormhole contract for a given EVM chain.
- * @param {EvmChain} chain The EVM chain to find the wormhole contract for.
- * @returns If found, the wormhole contract for the given EVM chain. Else, undefined
- */
- export function findWormholeContract(
- chain: EvmChain,
- ): EvmWormholeContract | undefined {
- for (const contract of Object.values(DefaultStore.wormhole_contracts)) {
- if (
- contract instanceof EvmWormholeContract &&
- contract.getChain().getId() === chain.getId()
- ) {
- return contract;
- }
- }
- }
- /**
- * Finds the executor contract for a given EVM chain.
- * @param {EvmChain} chain The EVM chain to find the executor contract for.
- * @returns If found, the executor contract for the given EVM chain. Else, undefined
- */
- export function findExecutorContract(
- chain: EvmChain,
- ): EvmExecutorContract | undefined {
- for (const contract of Object.values(DefaultStore.executor_contracts)) {
- if (
- contract instanceof EvmExecutorContract &&
- contract.chain.getId() === chain.getId()
- ) {
- console.log(
- `Found executor contract for ${chain.getId()} at ${contract.address}`,
- );
- return contract;
- }
- }
- }
- export type DeployWormholeReceiverContractsConfig = {
- saveContract: boolean;
- type: "stable" | "beta";
- } & BaseDeployConfig
- /**
- * Deploys the wormhole receiver contract for a given EVM chain.
- * @param {EvmChain} chain The EVM chain to find the wormhole receiver contract for.
- * @param {DeployWormholeReceiverContractsConfig} config The deployment configuration.
- * @param {string} cacheFile The path to the cache file.
- * @returns {EvmWormholeContract} The wormhole contract for the given EVM chain.
- */
- export async function deployWormholeContract(
- chain: EvmChain,
- config: DeployWormholeReceiverContractsConfig,
- cacheFile: string,
- ): Promise<EvmWormholeContract> {
- const receiverSetupAddr = await deployIfNotCached(
- cacheFile,
- chain,
- config,
- "ReceiverSetup",
- [],
- );
- const receiverImplAddr = await deployIfNotCached(
- cacheFile,
- chain,
- config,
- "ReceiverImplementation",
- [],
- );
- // Craft the init data for the proxy contract
- const setupContract = getWeb3Contract(
- config.jsonOutputDir,
- "ReceiverSetup",
- receiverSetupAddr,
- );
- const { wormholeConfig } = getDefaultDeploymentConfig(config.type);
- const initData = setupContract.methods
- .setup(
- receiverImplAddr,
- wormholeConfig.initialGuardianSet.map((addr: string) => "0x" + addr),
- chain.getWormholeChainId(),
- wormholeConfig.governanceChainId,
- "0x" + wormholeConfig.governanceContract,
- )
- .encodeABI();
- const wormholeReceiverAddr = await deployIfNotCached(
- cacheFile,
- chain,
- config,
- "WormholeReceiver",
- [receiverSetupAddr, initData],
- );
- const wormholeContract = new EvmWormholeContract(chain, wormholeReceiverAddr);
- if (config.type === "stable") {
- console.log(`Syncing mainnet guardian sets for ${chain.getId()}...`);
- // TODO: Add a way to pass gas configs to this
- await wormholeContract.syncMainnetGuardianSets(config.privateKey);
- console.log(`✅ Synced mainnet guardian sets for ${chain.getId()}`);
- }
- if (config.saveContract) {
- DefaultStore.wormhole_contracts[wormholeContract.getId()] =
- wormholeContract;
- DefaultStore.saveAllContracts();
- }
- return wormholeContract;
- }
- /**
- * Returns the wormhole contract for a given EVM chain.
- * If there was no wormhole contract deployed for the given chain, it will deploy the wormhole contract and save it to the default store.
- * @param {EvmChain} chain The EVM chain to find the wormhole contract for.
- * @param {DeployWormholeReceiverContractsConfig} config The deployment configuration.
- * @param {string} cacheFile The path to the cache file.
- * @returns {EvmWormholeContract} The wormhole contract for the given EVM chain.
- */
- export async function getOrDeployWormholeContract(
- chain: EvmChain,
- config: DeployWormholeReceiverContractsConfig,
- cacheFile: string,
- ): Promise<EvmWormholeContract> {
- return (
- findWormholeContract(chain) ??
- (await deployWormholeContract(chain, config, cacheFile))
- );
- }
- export type DefaultAddresses = {
- mainnet: string;
- testnet: string;
- }
- export async function topupAccountsIfNecessary(
- chain: EvmChain,
- deploymentConfig: BaseDeployConfig,
- accounts: [string, DefaultAddresses][],
- minBalance = 0.01,
- ) {
- for (const [accountName, defaultAddresses] of accounts) {
- const accountAddress = chain.isMainnet()
- ? defaultAddresses.mainnet
- : defaultAddresses.testnet;
- const web3 = chain.getWeb3();
- const balance = Number(
- web3.utils.fromWei(await web3.eth.getBalance(accountAddress), "ether"),
- );
- console.log(`${accountName} balance: ${balance} ETH`);
- if (balance < minBalance) {
- console.log(
- `Balance is less than ${minBalance}. Topping up the ${accountName} address...`,
- );
- const signer = web3.eth.accounts.privateKeyToAccount(
- deploymentConfig.privateKey,
- );
- web3.eth.accounts.wallet.add(signer);
- const estimatedGas = await web3.eth.estimateGas({
- from: signer.address,
- to: accountAddress,
- value: web3.utils.toWei(`${minBalance}`, "ether"),
- });
- const tx = await web3.eth.sendTransaction({
- from: signer.address,
- to: accountAddress,
- gas: estimatedGas * deploymentConfig.gasMultiplier,
- value: web3.utils.toWei(`${minBalance}`, "ether"),
- });
- console.log(
- `Topped up the ${accountName} address. Tx:`,
- tx.transactionHash,
- );
- }
- }
- }
|