common.ts 12 KB

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