common.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  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(
  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 interface DeployWormholeReceiverContractsConfig
  261. extends BaseDeployConfig {
  262. saveContract: boolean;
  263. type: "stable" | "beta";
  264. }
  265. /**
  266. * Deploys the wormhole receiver contract for a given EVM chain.
  267. * @param {EvmChain} chain The EVM chain to find the wormhole receiver contract for.
  268. * @param {DeployWormholeReceiverContractsConfig} config The deployment configuration.
  269. * @param {string} cacheFile The path to the cache file.
  270. * @returns {EvmWormholeContract} The wormhole contract for the given EVM chain.
  271. */
  272. export async function deployWormholeContract(
  273. chain: EvmChain,
  274. config: DeployWormholeReceiverContractsConfig,
  275. cacheFile: string,
  276. ): Promise<EvmWormholeContract> {
  277. const receiverSetupAddr = await deployIfNotCached(
  278. cacheFile,
  279. chain,
  280. config,
  281. "ReceiverSetup",
  282. [],
  283. );
  284. const receiverImplAddr = await deployIfNotCached(
  285. cacheFile,
  286. chain,
  287. config,
  288. "ReceiverImplementation",
  289. [],
  290. );
  291. // Craft the init data for the proxy contract
  292. const setupContract = getWeb3Contract(
  293. config.jsonOutputDir,
  294. "ReceiverSetup",
  295. receiverSetupAddr,
  296. );
  297. const { wormholeConfig } = getDefaultDeploymentConfig(config.type);
  298. const initData = setupContract.methods
  299. .setup(
  300. receiverImplAddr,
  301. wormholeConfig.initialGuardianSet.map((addr: string) => "0x" + addr),
  302. chain.getWormholeChainId(),
  303. wormholeConfig.governanceChainId,
  304. "0x" + wormholeConfig.governanceContract,
  305. )
  306. .encodeABI();
  307. const wormholeReceiverAddr = await deployIfNotCached(
  308. cacheFile,
  309. chain,
  310. config,
  311. "WormholeReceiver",
  312. [receiverSetupAddr, initData],
  313. );
  314. const wormholeContract = new EvmWormholeContract(chain, wormholeReceiverAddr);
  315. if (config.type === "stable") {
  316. console.log(`Syncing mainnet guardian sets for ${chain.getId()}...`);
  317. // TODO: Add a way to pass gas configs to this
  318. await wormholeContract.syncMainnetGuardianSets(config.privateKey);
  319. console.log(`✅ Synced mainnet guardian sets for ${chain.getId()}`);
  320. }
  321. if (config.saveContract) {
  322. DefaultStore.wormhole_contracts[wormholeContract.getId()] =
  323. wormholeContract;
  324. DefaultStore.saveAllContracts();
  325. }
  326. return wormholeContract;
  327. }
  328. /**
  329. * Returns the wormhole contract for a given EVM chain.
  330. * If there was no wormhole contract deployed for the given chain, it will deploy the wormhole contract and save it to the default store.
  331. * @param {EvmChain} chain The EVM chain to find the wormhole contract for.
  332. * @param {DeployWormholeReceiverContractsConfig} config The deployment configuration.
  333. * @param {string} cacheFile The path to the cache file.
  334. * @returns {EvmWormholeContract} The wormhole contract for the given EVM chain.
  335. */
  336. export async function getOrDeployWormholeContract(
  337. chain: EvmChain,
  338. config: DeployWormholeReceiverContractsConfig,
  339. cacheFile: string,
  340. ): Promise<EvmWormholeContract> {
  341. return (
  342. findWormholeContract(chain) ??
  343. (await deployWormholeContract(chain, config, cacheFile))
  344. );
  345. }
  346. export interface DefaultAddresses {
  347. mainnet: string;
  348. testnet: string;
  349. }
  350. export async function topupAccountsIfNecessary(
  351. chain: EvmChain,
  352. deploymentConfig: BaseDeployConfig,
  353. accounts: Array<[string, DefaultAddresses]>,
  354. minBalance = 0.01,
  355. ) {
  356. for (const [accountName, defaultAddresses] of accounts) {
  357. const accountAddress = chain.isMainnet()
  358. ? defaultAddresses.mainnet
  359. : defaultAddresses.testnet;
  360. const web3 = chain.getWeb3();
  361. const balance = Number(
  362. web3.utils.fromWei(await web3.eth.getBalance(accountAddress), "ether"),
  363. );
  364. console.log(`${accountName} balance: ${balance} ETH`);
  365. if (balance < minBalance) {
  366. console.log(
  367. `Balance is less than ${minBalance}. Topping up the ${accountName} address...`,
  368. );
  369. const signer = web3.eth.accounts.privateKeyToAccount(
  370. deploymentConfig.privateKey,
  371. );
  372. web3.eth.accounts.wallet.add(signer);
  373. const estimatedGas = await web3.eth.estimateGas({
  374. from: signer.address,
  375. to: accountAddress,
  376. value: web3.utils.toWei(`${minBalance}`, "ether"),
  377. });
  378. const tx = await web3.eth.sendTransaction({
  379. from: signer.address,
  380. to: accountAddress,
  381. gas: estimatedGas * deploymentConfig.gasMultiplier,
  382. value: web3.utils.toWei(`${minBalance}`, "ether"),
  383. });
  384. console.log(
  385. `Topped up the ${accountName} address. Tx: `,
  386. tx.transactionHash,
  387. );
  388. }
  389. }
  390. }