common.ts 13 KB

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