common.ts 13 KB

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