From 03c554812f6e92922a6f9fc6e671c235f07591c8 Mon Sep 17 00:00:00 2001 From: Aditya Arora Date: Tue, 5 May 2026 15:22:00 -0400 Subject: [PATCH] feat(contract-manager): multi-contract support + safety in sync_governance_vaas Adds the ability to sync many contracts in one invocation and a few safety improvements to the script that came out of running the q2 fee proposals. New CLI options: --contract repeatable; replaces the previous single-contract form --contracts-file

newline-separated file of ids (supports # comments) --all iterate every mainnet pricefeed/executor contract --gas-price-gwei override gasPrice; useful on chains with fast-moving base fees (e.g. Arbitrum) --dry-run decode and print without submitting transactions Bug fixes / behaviour: - fall back to DefaultStore.executor_contracts so executor contract ids resolve (previously threw "Contract not found") - coerce lastExecutedGovernanceSequence to Number; EvmExecutorContract returns a string so "lastExecuted + 1" was string-concatenating - use contract.chain.wormholeChainName instead of contract.getChain().*; EvmExecutorContract has no getChain() - wrap executeGovernanceInstruction in try/catch so a single bad VAA (e.g. signed by an expired guardian set) doesn't kill the whole loop - log a friendly "reached end of VAA queue" instead of dumping a stack trace when fetchVaa fails at end-of-queue - skip non-EVM contracts gracefully when iterating with --all Co-Authored-By: Claude Opus 4.7 (1M context) --- .../scripts/sync_governance_vaas.ts | 237 ++++++++++++------ 1 file changed, 164 insertions(+), 73 deletions(-) diff --git a/contract_manager/scripts/sync_governance_vaas.ts b/contract_manager/scripts/sync_governance_vaas.ts index e8958ffd89..cd683dc9e3 100644 --- a/contract_manager/scripts/sync_governance_vaas.ts +++ b/contract_manager/scripts/sync_governance_vaas.ts @@ -1,113 +1,204 @@ +/** biome-ignore-all lint/suspicious/noConsole: this is a CLI script */ +/** biome-ignore-all lint/style/noNonNullAssertion: store lookups are always present */ /* eslint-disable no-console */ /* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable unicorn/no-await-expression-member */ +import fs from "node:fs"; + import { parseVaa } from "@certusone/wormhole-sdk"; import { decodeGovernancePayload } from "@pythnetwork/xc-admin-common"; import yargs from "yargs"; import { hideBin } from "yargs/helpers"; import { toPrivateKey } from "../src/core/base"; -import { SubmittedWormholeMessage, Vault } from "../src/node/utils/governance"; +import { + EvmExecutorContract, + EvmPriceFeedContract, +} from "../src/core/contracts/evm"; +import type { Vault } from "../src/node/utils/governance"; +import { SubmittedWormholeMessage } from "../src/node/utils/governance"; import { DefaultStore } from "../src/node/utils/store"; const parser = yargs(hideBin(process.argv)) .usage( - "Tries to execute all vaas on a contract.\n" + + "Tries to execute all vaas on one or more contracts.\n" + "Useful for recently deployed contracts.\n" + - "Usage: $0 --contract --private-key ", + "Usage: $0 (--contract | --contracts-file | --all) --private-key ", ) .options({ + all: { + default: false, + desc: "Sync all mainnet pricefeed/executor contracts in the store.", + type: "boolean", + }, contract: { + array: true, + desc: "Contract id(s) to execute governance vaas for. Can be passed multiple times.", type: "string", - demandOption: true, - desc: "Contract to execute governance vaas for", }, - "private-key": { + "contracts-file": { + desc: "Path to a newline-separated file of contract ids.", type: "string", - demandOption: true, - desc: "Private key to sign the transactions executing the governance VAAs. Hex format, without 0x prefix.", }, - offset: { + "dry-run": { + default: false, + desc: "Decode and print what would be executed; do not submit transactions.", + type: "boolean", + }, + "gas-price-gwei": { + desc: "Override gasPrice (gwei). Useful on chains with fast-moving base fees.", type: "number", + }, + offset: { desc: "Starting sequence number to use, if not provided will start from contract last executed governance sequence number", + type: "number", + }, + "private-key": { + demandOption: true, + desc: "Private key to sign the transactions executing the governance VAAs. Hex format, without 0x prefix.", + type: "string", }, }); async function main() { const argv = await parser.argv; - const contract = DefaultStore.contracts[argv.contract]; - if (!contract) { - throw new Error(`Contract ${argv.contract} not found`); + + const contracts: (EvmPriceFeedContract | EvmExecutorContract)[] = []; + if (argv.all) { + for (const c of Object.values(DefaultStore.contracts)) { + if (c instanceof EvmPriceFeedContract && c.chain.isMainnet()) + contracts.push(c); + } + for (const c of Object.values(DefaultStore.executor_contracts)) { + if (c.chain.isMainnet()) contracts.push(c); + } } - const governanceSource = await contract.getGovernanceDataSource(); - const mainnetVault = - DefaultStore.vaults[ - "mainnet-beta_FVQyHcooAtThJ83XFrNnv74BcinbRH3bRmfFamAHBfuj" - ]!; - const devnetVault = - DefaultStore.vaults.devnet_6baWtW1zTUVMSJHJQVxDUXWzqrQeYBr6mu31j3bTKwY3!; - let matchedVault: Vault; - if ( - (await devnetVault.getEmitter()).toBuffer().toString("hex") === - governanceSource.emitterAddress - ) { - console.log("devnet multisig matches governance source"); - matchedVault = devnetVault; - } else if ( - (await mainnetVault.getEmitter()).toBuffer().toString("hex") === - governanceSource.emitterAddress - ) { - console.log("mainnet multisig matches governance source"); - matchedVault = mainnetVault; - } else { - throw new Error( - "can not find a multisig that matches the governance source of the contract", + const ids = [...(argv.contract ?? [])]; + if (argv["contracts-file"]) { + ids.push( + ...fs + .readFileSync(argv["contracts-file"], "utf8") + .split("\n") + .map((l) => l.trim()) + .filter((l) => l.length > 0 && !l.startsWith("#")), ); } - let lastExecuted = await contract.getLastExecutedGovernanceSequence(); - console.log("last executed governance sequence", lastExecuted); - if (argv.offset && argv.offset > lastExecuted) { - console.log("skipping to offset", argv.offset); - lastExecuted = argv.offset - 1; + for (const id of ids) { + const c = DefaultStore.contracts[id] || DefaultStore.executor_contracts[id]; + if (!c) { + console.warn(`[WARN] contract ${id} not found, skipping`); + continue; + } + if ( + !(c instanceof EvmPriceFeedContract || c instanceof EvmExecutorContract) + ) { + console.warn(`[WARN] contract ${id} is not an EVM contract, skipping`); + continue; + } + contracts.push(c); + } + if (contracts.length === 0) { + throw new Error("Must provide --contract, --contracts-file, or --all"); } - console.log("Starting from sequence number", lastExecuted); - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - while (true) { - const submittedWormholeMessage = new SubmittedWormholeMessage( - await matchedVault.getEmitter(), - lastExecuted + 1, - matchedVault.cluster, - ); - let vaa: Buffer; - try { - vaa = await submittedWormholeMessage.fetchVaa(); - } catch (error) { - console.log(error); - console.log("no vaa found for sequence", lastExecuted + 1); - break; - } - const parsedVaa = parseVaa(vaa); - const action = decodeGovernancePayload(parsedVaa.payload); - if (!action) { - console.log("can not decode vaa, skipping"); + for (const contract of contracts) { + if (contracts.length > 1) console.log(`=== ${contract.getId()} ===`); + + const governanceSource = await contract.getGovernanceDataSource(); + const mainnetVault = + DefaultStore.vaults[ + "mainnet-beta_FVQyHcooAtThJ83XFrNnv74BcinbRH3bRmfFamAHBfuj" + ]!; + const devnetVault = + DefaultStore.vaults.devnet_6baWtW1zTUVMSJHJQVxDUXWzqrQeYBr6mu31j3bTKwY3!; + let matchedVault: Vault; + if ( + (await devnetVault.getEmitter()).toBuffer().toString("hex") === + governanceSource.emitterAddress + ) { + console.log("devnet multisig matches governance source"); + matchedVault = devnetVault; } else if ( - action.targetChainId === "unset" || - contract.getChain().wormholeChainName === action.targetChainId + (await mainnetVault.getEmitter()).toBuffer().toString("hex") === + governanceSource.emitterAddress ) { - console.log("executing vaa", lastExecuted + 1); - await contract.executeGovernanceInstruction( - toPrivateKey(argv["private-key"]), - vaa, - ); + console.log("mainnet multisig matches governance source"); + matchedVault = mainnetVault; } else { - console.log( - `vaa is not for this chain (${ - contract.getChain().wormholeChainName - } != ${action.targetChainId}, skipping`, - ); + console.log("no multisig matches governance source, skipping"); + continue; + } + // EvmExecutorContract returns a string, EvmPriceFeedContract returns a number; coerce to be safe. + let lastExecuted = Number( + await contract.getLastExecutedGovernanceSequence(), + ); + console.log("last executed governance sequence", lastExecuted); + if (argv.offset && argv.offset > lastExecuted) { + console.log("skipping to offset", argv.offset); + lastExecuted = argv.offset - 1; + } + console.log("Starting from sequence number", lastExecuted); + + // Optional gas-price override (e.g. for Arbitrum's fast-moving base fee). + const originalGetGasPrice = contract.chain.getGasPrice.bind(contract.chain); + if (argv["gas-price-gwei"] !== undefined) { + const gasPriceWei = Math.trunc(argv["gas-price-gwei"] * 1e9).toString(); + contract.chain.getGasPrice = async () => gasPriceWei; + } + + try { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + while (true) { + const submittedWormholeMessage = new SubmittedWormholeMessage( + await matchedVault.getEmitter(), + lastExecuted + 1, + matchedVault.cluster, + ); + let vaa: Buffer; + try { + vaa = await submittedWormholeMessage.fetchVaa(); + } catch { + console.log( + `reached end of VAA queue at sequence ${lastExecuted + 1}`, + ); + break; + } + const parsedVaa = parseVaa(vaa); + const action = decodeGovernancePayload(parsedVaa.payload); + if (!action) { + console.log("can not decode vaa, skipping"); + } else if ( + action.targetChainId === "unset" || + contract.chain.wormholeChainName === action.targetChainId + ) { + if (argv["dry-run"]) { + console.log(`[dry-run] would execute vaa ${lastExecuted + 1}`); + } else { + console.log("executing vaa", lastExecuted + 1); + try { + await contract.executeGovernanceInstruction( + toPrivateKey(argv["private-key"]), + vaa, + ); + } catch (error) { + console.log( + `failed to execute vaa ${lastExecuted + 1}, continuing:`, + (error as Error).message, + ); + } + } + } else { + console.log( + `vaa is not for this chain (${ + contract.chain.wormholeChainName + } != ${action.targetChainId}, skipping`, + ); + } + lastExecuted++; + } + } finally { + contract.chain.getGasPrice = originalGetGasPrice; } - lastExecuted++; } }