common.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. import { DefaultStore } from "../src/node/utils/store";
  2. import { existsSync, readFileSync, writeFileSync } from "fs";
  3. import { join } from "path";
  4. import Web3 from "web3";
  5. import { Contract } from "web3-eth-contract";
  6. import { InferredOptionType } from "yargs";
  7. import { PrivateKey, getDefaultDeploymentConfig } from "../src/core/base";
  8. import { EvmChain } from "../src/core/chains";
  9. import {
  10. EvmEntropyContract,
  11. EvmExecutorContract,
  12. EvmWormholeContract,
  13. } from "../src/core/contracts";
  14. export interface BaseDeployConfig {
  15. gasMultiplier: number;
  16. gasPriceMultiplier: number;
  17. jsonOutputDir: string;
  18. privateKey: PrivateKey;
  19. }
  20. // Deploys a contract if it was not deployed before.
  21. // It will check for the past deployments in file `cacheFile` against a key
  22. // If `cacheKey` is provided it will be used as the key, else it will use
  23. // a key - `${chain.getId()}-${artifactName}`
  24. export async function deployIfNotCached(
  25. cacheFile: string,
  26. chain: EvmChain,
  27. config: BaseDeployConfig,
  28. artifactName: string,
  29. deployArgs: any[], // eslint-disable-line @typescript-eslint/no-explicit-any
  30. cacheKey?: string,
  31. ): Promise<string> {
  32. const runIfNotCached = makeCacheFunction(cacheFile);
  33. const key = cacheKey ?? `${chain.getId()}-${artifactName}`;
  34. return runIfNotCached(key, async () => {
  35. const artifact = JSON.parse(
  36. readFileSync(join(config.jsonOutputDir, `${artifactName}.json`), "utf8"),
  37. );
  38. // Handle bytecode which can be either a string or an object with an 'object' property
  39. let bytecode = artifact["bytecode"];
  40. if (
  41. typeof bytecode === "object" &&
  42. bytecode !== null &&
  43. "object" in bytecode
  44. ) {
  45. bytecode = bytecode.object;
  46. }
  47. // Ensure bytecode starts with 0x
  48. if (!bytecode.startsWith("0x")) {
  49. bytecode = `0x${bytecode}`;
  50. }
  51. console.log(`Deploying ${artifactName} on ${chain.getId()}...`);
  52. const addr = await chain.deploy(
  53. config.privateKey,
  54. artifact["abi"],
  55. bytecode,
  56. deployArgs,
  57. config.gasMultiplier,
  58. config.gasPriceMultiplier,
  59. );
  60. console.log(`✅ Deployed ${artifactName} on ${chain.getId()} at ${addr}`);
  61. return addr;
  62. });
  63. }
  64. export function getWeb3Contract(
  65. jsonOutputDir: string,
  66. artifactName: string,
  67. address: string,
  68. ): Contract {
  69. const artifact = JSON.parse(
  70. readFileSync(join(jsonOutputDir, `${artifactName}.json`), "utf8"),
  71. );
  72. const web3 = new Web3();
  73. return new web3.eth.Contract(artifact["abi"], address);
  74. }
  75. export const COMMON_DEPLOY_OPTIONS = {
  76. "std-output-dir": {
  77. type: "string",
  78. demandOption: true,
  79. desc: "Path to the standard JSON output of the contracts (build artifact) directory",
  80. },
  81. "private-key": {
  82. type: "string",
  83. demandOption: true,
  84. desc: "Private key to sign the transactions with",
  85. },
  86. chain: {
  87. type: "array",
  88. string: true,
  89. demandOption: true,
  90. desc: "Chains to upload the contract on. Must be one of the chains available in the store",
  91. },
  92. "deployment-type": {
  93. type: "string",
  94. demandOption: false,
  95. default: "stable",
  96. desc: "Deployment type to use. Can be 'stable' or 'beta'",
  97. },
  98. "gas-multiplier": {
  99. type: "number",
  100. demandOption: false,
  101. // Proxy (ERC1967) contract gas estimate is insufficient in many networks and thus we use 2 by default to make it work.
  102. default: 2,
  103. desc: "Gas multiplier to use for the deployment. This is useful when gas estimates are not accurate",
  104. },
  105. "gas-price-multiplier": {
  106. type: "number",
  107. demandOption: false,
  108. default: 1,
  109. desc: "Gas price multiplier to use for the deployment. This is useful when gas price estimates are not accurate",
  110. },
  111. "save-contract": {
  112. type: "boolean",
  113. demandOption: false,
  114. default: true,
  115. desc: "Save the contract to the store",
  116. },
  117. } as const;
  118. export const CHAIN_SELECTION_OPTIONS = {
  119. testnet: {
  120. type: "boolean",
  121. default: false,
  122. desc: "Upgrade testnet contracts instead of mainnet",
  123. },
  124. "all-chains": {
  125. type: "boolean",
  126. default: false,
  127. desc: "Upgrade the contract on all chains. Use with --testnet flag to upgrade all testnet contracts",
  128. },
  129. chain: {
  130. type: "array",
  131. string: true,
  132. desc: "Chains to upgrade the contract on",
  133. },
  134. } as const;
  135. export const COMMON_UPGRADE_OPTIONS = {
  136. ...CHAIN_SELECTION_OPTIONS,
  137. "private-key": COMMON_DEPLOY_OPTIONS["private-key"],
  138. "ops-key-path": {
  139. type: "string",
  140. demandOption: true,
  141. desc: "Path to the private key of the proposer to use for the operations multisig governance proposal",
  142. },
  143. "std-output": {
  144. type: "string",
  145. demandOption: true,
  146. desc: "Path to the standard JSON output of the pyth contract (build artifact)",
  147. },
  148. } as const;
  149. export function makeCacheFunction(
  150. cacheFile: string,
  151. ): (cacheKey: string, fn: () => Promise<string>) => Promise<string> {
  152. async function runIfNotCached(
  153. cacheKey: string,
  154. fn: () => Promise<string>,
  155. ): Promise<string> {
  156. const cache = existsSync(cacheFile)
  157. ? JSON.parse(readFileSync(cacheFile, "utf8"))
  158. : {};
  159. if (cache[cacheKey]) {
  160. return cache[cacheKey];
  161. }
  162. const result = await fn();
  163. cache[cacheKey] = result;
  164. writeFileSync(cacheFile, JSON.stringify(cache, null, 2));
  165. return result;
  166. }
  167. return runIfNotCached;
  168. }
  169. export function getSelectedChains(argv: {
  170. chain: InferredOptionType<(typeof COMMON_UPGRADE_OPTIONS)["chain"]>;
  171. testnet: InferredOptionType<(typeof COMMON_UPGRADE_OPTIONS)["testnet"]>;
  172. allChains: InferredOptionType<(typeof COMMON_UPGRADE_OPTIONS)["all-chains"]>;
  173. }) {
  174. const selectedChains: EvmChain[] = [];
  175. if (argv.allChains && argv.chain)
  176. throw new Error("Cannot use both --all-chains and --chain");
  177. if (!argv.allChains && !argv.chain)
  178. throw new Error("Must use either --all-chains or --chain");
  179. for (const chain of Object.values(DefaultStore.chains)) {
  180. if (!(chain instanceof EvmChain)) continue;
  181. if (
  182. (argv.allChains && chain.isMainnet() !== argv.testnet) ||
  183. argv.chain?.includes(chain.getId())
  184. )
  185. selectedChains.push(chain);
  186. }
  187. if (argv.chain && selectedChains.length !== argv.chain.length)
  188. throw new Error(
  189. `Some chains were not found ${selectedChains
  190. .map((chain) => chain.getId())
  191. .toString()}`,
  192. );
  193. for (const chain of selectedChains) {
  194. if (chain.isMainnet() != selectedChains[0].isMainnet())
  195. throw new Error("All chains must be either mainnet or testnet");
  196. }
  197. return selectedChains;
  198. }
  199. /**
  200. * Finds the entropy contract for a given EVM chain.
  201. * @param {EvmChain} chain The EVM chain to find the entropy contract for.
  202. * @returns The entropy contract for the given EVM chain.
  203. * @throws {Error} an error if the entropy contract is not found for the given EVM chain.
  204. */
  205. export function findEntropyContract(chain: EvmChain): EvmEntropyContract {
  206. for (const contract of Object.values(DefaultStore.entropy_contracts)) {
  207. if (contract.getChain().getId() === chain.getId()) {
  208. return contract;
  209. }
  210. }
  211. throw new Error(`Entropy contract not found for chain ${chain.getId()}`);
  212. }
  213. /**
  214. * Finds the wormhole contract for a given EVM chain.
  215. * @param {EvmChain} chain The EVM chain to find the wormhole contract for.
  216. * @returns If found, the wormhole contract for the given EVM chain. Else, undefined
  217. */
  218. export function findWormholeContract(
  219. chain: EvmChain,
  220. ): EvmWormholeContract | undefined {
  221. for (const contract of Object.values(DefaultStore.wormhole_contracts)) {
  222. if (
  223. contract instanceof EvmWormholeContract &&
  224. contract.getChain().getId() === chain.getId()
  225. ) {
  226. return contract;
  227. }
  228. }
  229. }
  230. /**
  231. * Finds the executor contract for a given EVM chain.
  232. * @param {EvmChain} chain The EVM chain to find the executor contract for.
  233. * @returns If found, the executor contract for the given EVM chain. Else, undefined
  234. */
  235. export function findExecutorContract(
  236. chain: EvmChain,
  237. ): EvmExecutorContract | undefined {
  238. for (const contract of Object.values(DefaultStore.executor_contracts)) {
  239. if (
  240. contract instanceof EvmExecutorContract &&
  241. contract.chain.getId() === chain.getId()
  242. ) {
  243. console.log(
  244. `Found executor contract for ${chain.getId()} at ${contract.address}`,
  245. );
  246. return contract;
  247. }
  248. }
  249. }
  250. export interface DeployWormholeReceiverContractsConfig
  251. extends BaseDeployConfig {
  252. saveContract: boolean;
  253. type: "stable" | "beta";
  254. }
  255. /**
  256. * Deploys the wormhole receiver contract for a given EVM chain.
  257. * @param {EvmChain} chain The EVM chain to find the wormhole receiver contract for.
  258. * @param {DeployWormholeReceiverContractsConfig} config The deployment configuration.
  259. * @param {string} cacheFile The path to the cache file.
  260. * @returns {EvmWormholeContract} The wormhole contract for the given EVM chain.
  261. */
  262. export async function deployWormholeContract(
  263. chain: EvmChain,
  264. config: DeployWormholeReceiverContractsConfig,
  265. cacheFile: string,
  266. ): Promise<EvmWormholeContract> {
  267. const receiverSetupAddr = await deployIfNotCached(
  268. cacheFile,
  269. chain,
  270. config,
  271. "ReceiverSetup",
  272. [],
  273. );
  274. const receiverImplAddr = await deployIfNotCached(
  275. cacheFile,
  276. chain,
  277. config,
  278. "ReceiverImplementation",
  279. [],
  280. );
  281. // Craft the init data for the proxy contract
  282. const setupContract = getWeb3Contract(
  283. config.jsonOutputDir,
  284. "ReceiverSetup",
  285. receiverSetupAddr,
  286. );
  287. const { wormholeConfig } = getDefaultDeploymentConfig(config.type);
  288. const initData = setupContract.methods
  289. .setup(
  290. receiverImplAddr,
  291. wormholeConfig.initialGuardianSet.map((addr: string) => "0x" + addr),
  292. chain.getWormholeChainId(),
  293. wormholeConfig.governanceChainId,
  294. "0x" + wormholeConfig.governanceContract,
  295. )
  296. .encodeABI();
  297. const wormholeReceiverAddr = await deployIfNotCached(
  298. cacheFile,
  299. chain,
  300. config,
  301. "WormholeReceiver",
  302. [receiverSetupAddr, initData],
  303. );
  304. const wormholeContract = new EvmWormholeContract(chain, wormholeReceiverAddr);
  305. if (config.type === "stable") {
  306. console.log(`Syncing mainnet guardian sets for ${chain.getId()}...`);
  307. // TODO: Add a way to pass gas configs to this
  308. await wormholeContract.syncMainnetGuardianSets(config.privateKey);
  309. console.log(`✅ Synced mainnet guardian sets for ${chain.getId()}`);
  310. }
  311. if (config.saveContract) {
  312. DefaultStore.wormhole_contracts[wormholeContract.getId()] =
  313. wormholeContract;
  314. DefaultStore.saveAllContracts();
  315. }
  316. return wormholeContract;
  317. }
  318. /**
  319. * Returns the wormhole contract for a given EVM chain.
  320. * If there was no wormhole contract deployed for the given chain, it will deploy the wormhole contract and save it to the default store.
  321. * @param {EvmChain} chain The EVM chain to find the wormhole contract for.
  322. * @param {DeployWormholeReceiverContractsConfig} config The deployment configuration.
  323. * @param {string} cacheFile The path to the cache file.
  324. * @returns {EvmWormholeContract} The wormhole contract for the given EVM chain.
  325. */
  326. export async function getOrDeployWormholeContract(
  327. chain: EvmChain,
  328. config: DeployWormholeReceiverContractsConfig,
  329. cacheFile: string,
  330. ): Promise<EvmWormholeContract> {
  331. return (
  332. findWormholeContract(chain) ??
  333. (await deployWormholeContract(chain, config, cacheFile))
  334. );
  335. }
  336. export interface DefaultAddresses {
  337. mainnet: string;
  338. testnet: string;
  339. }
  340. export async function topupAccountsIfNecessary(
  341. chain: EvmChain,
  342. deploymentConfig: BaseDeployConfig,
  343. accounts: Array<[string, DefaultAddresses]>,
  344. minBalance = 0.01,
  345. ) {
  346. for (const [accountName, defaultAddresses] of accounts) {
  347. const accountAddress = chain.isMainnet()
  348. ? defaultAddresses.mainnet
  349. : defaultAddresses.testnet;
  350. const web3 = chain.getWeb3();
  351. const balance = Number(
  352. web3.utils.fromWei(await web3.eth.getBalance(accountAddress), "ether"),
  353. );
  354. console.log(`${accountName} balance: ${balance} ETH`);
  355. if (balance < minBalance) {
  356. console.log(
  357. `Balance is less than ${minBalance}. Topping up the ${accountName} address...`,
  358. );
  359. const signer = web3.eth.accounts.privateKeyToAccount(
  360. deploymentConfig.privateKey,
  361. );
  362. web3.eth.accounts.wallet.add(signer);
  363. const estimatedGas = await web3.eth.estimateGas({
  364. from: signer.address,
  365. to: accountAddress,
  366. value: web3.utils.toWei(`${minBalance}`, "ether"),
  367. });
  368. const tx = await web3.eth.sendTransaction({
  369. from: signer.address,
  370. to: accountAddress,
  371. gas: estimatedGas * deploymentConfig.gasMultiplier,
  372. value: web3.utils.toWei(`${minBalance}`, "ether"),
  373. });
  374. console.log(
  375. `Topped up the ${accountName} address. Tx: `,
  376. tx.transactionHash,
  377. );
  378. }
  379. }
  380. }