From 75caa147ea0ff2f064dedfac16b50fe30e9df6d9 Mon Sep 17 00:00:00 2001 From: dule-git <61541725+dule-git@users.noreply.github.com> Date: Mon, 23 Sep 2024 16:16:12 +0200 Subject: [PATCH] Add hre extender v2 (#204) * extract ethers from hardhat-tenderly-core * modify build and clean command on tenderly-core * add hre-extender-v1 * add hre-extender-v2 --- packages/hre-extender-v2/.eslintrc.js | 7 + packages/hre-extender-v2/.gitignore | 80 ++++++ packages/hre-extender-v2/.npmignore | 17 ++ packages/hre-extender-v2/.prettierignore | 14 + packages/hre-extender-v2/LICENCE | 21 ++ packages/hre-extender-v2/package.json | 45 ++++ .../src/extenders/extend-ethers.ts | 244 ++++++++++++++++++ .../src/extenders/extend-hardhat-deploy.ts | 55 ++++ .../src/extenders/extend-upgrades.ts | 160 ++++++++++++ .../populate-hardhat-verify-config.ts | 125 +++++++++ .../extenders/tenderly-network-resolver.ts | 40 +++ packages/hre-extender-v2/src/index.ts | 6 + packages/hre-extender-v2/src/logger.ts | 6 + packages/hre-extender-v2/src/setup.ts | 185 +++++++++++++ .../src/types/ContractByName.ts | 8 + .../hre-extender-v2/src/types/TdlyContract.ts | 133 ++++++++++ .../src/types/TdlyContractFactory.ts | 98 +++++++ .../src/types/TdlyProxyContract.ts | 45 ++++ packages/hre-extender-v2/tsconfig.json | 18 ++ 19 files changed, 1307 insertions(+) create mode 100644 packages/hre-extender-v2/.eslintrc.js create mode 100644 packages/hre-extender-v2/.gitignore create mode 100644 packages/hre-extender-v2/.npmignore create mode 100644 packages/hre-extender-v2/.prettierignore create mode 100644 packages/hre-extender-v2/LICENCE create mode 100644 packages/hre-extender-v2/package.json create mode 100644 packages/hre-extender-v2/src/extenders/extend-ethers.ts create mode 100644 packages/hre-extender-v2/src/extenders/extend-hardhat-deploy.ts create mode 100644 packages/hre-extender-v2/src/extenders/extend-upgrades.ts create mode 100644 packages/hre-extender-v2/src/extenders/populate-hardhat-verify-config.ts create mode 100644 packages/hre-extender-v2/src/extenders/tenderly-network-resolver.ts create mode 100644 packages/hre-extender-v2/src/index.ts create mode 100644 packages/hre-extender-v2/src/logger.ts create mode 100644 packages/hre-extender-v2/src/setup.ts create mode 100644 packages/hre-extender-v2/src/types/ContractByName.ts create mode 100644 packages/hre-extender-v2/src/types/TdlyContract.ts create mode 100644 packages/hre-extender-v2/src/types/TdlyContractFactory.ts create mode 100644 packages/hre-extender-v2/src/types/TdlyProxyContract.ts create mode 100644 packages/hre-extender-v2/tsconfig.json diff --git a/packages/hre-extender-v2/.eslintrc.js b/packages/hre-extender-v2/.eslintrc.js new file mode 100644 index 00000000..9c4a447b --- /dev/null +++ b/packages/hre-extender-v2/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: [`${__dirname}/../../config/eslint/eslintrc.js`], + parserOptions: { + project: `${__dirname}/tsconfig.json`, + sourceType: "module" + }, +}; \ No newline at end of file diff --git a/packages/hre-extender-v2/.gitignore b/packages/hre-extender-v2/.gitignore new file mode 100644 index 00000000..e270d0fe --- /dev/null +++ b/packages/hre-extender-v2/.gitignore @@ -0,0 +1,80 @@ +/dist +tsconfig.tsbuildinfo + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# next.js build output +.next + +# TSC prod output. This has to be in sync with the to directories in ./src, and with npm script clean +/*.js +/*.js.map +/*.d.ts +/*.d.ts.map +/builtin-tasks +/common +/internal +/types +/utils + +# Tests compilation output +/build-test/ + +!.eslintrc.js +.env +.idea +.vscode \ No newline at end of file diff --git a/packages/hre-extender-v2/.npmignore b/packages/hre-extender-v2/.npmignore new file mode 100644 index 00000000..aa333d09 --- /dev/null +++ b/packages/hre-extender-v2/.npmignore @@ -0,0 +1,17 @@ +src/ + +.env + +.editorconfig + +tsconfig.json + +tslint.json + +.idea + +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* \ No newline at end of file diff --git a/packages/hre-extender-v2/.prettierignore b/packages/hre-extender-v2/.prettierignore new file mode 100644 index 00000000..b58afd52 --- /dev/null +++ b/packages/hre-extender-v2/.prettierignore @@ -0,0 +1,14 @@ +/dist +/node_modules +/build-test +/builtin-tasks +/common +/internal +/types +/utils +/*.d.ts +/*.d.ts.map +/*.js +/*.js.map +/.nyc_output +CHANGELOG.md \ No newline at end of file diff --git a/packages/hre-extender-v2/LICENCE b/packages/hre-extender-v2/LICENCE new file mode 100644 index 00000000..19383c34 --- /dev/null +++ b/packages/hre-extender-v2/LICENCE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Tenderly + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/hre-extender-v2/package.json b/packages/hre-extender-v2/package.json new file mode 100644 index 00000000..186d650b --- /dev/null +++ b/packages/hre-extender-v2/package.json @@ -0,0 +1,45 @@ +{ + "name": "@tenderly/hre-extender-v2", + "author": "Tenderly", + "license": "MIT", + "homepage": "https://tenderly.co", + "description": "Package for overloading some of the HardhatRuntimeEnvironment components", + "version": "0.0.0", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/Tenderly/hardhat-tenderly.git" + }, + "keywords": [ + "ethereum", + "smart-contracts", + "hardhat", + "hardhat-plugin", + "tenderly" + ], + "files": [ + "dist/", + "LICENSE", + "README.md" + ], + "scripts": { + "build": "rm -rf ./dist && rm -f tsconfig.tsbuildinfo && tsc --build .", + "clean": "rm -rf node_modules && rm -rf dist && rm -f tsconfig.tsbuildinfo", + "lint": "yarn run prettier --check && yarn run eslint", + "lint:fix": "yarn run prettier --write && yarn run eslint --fix", + "eslint": "eslint 'src/**/*.ts'", + "prettier": "prettier \"**/*.{js,md,json}\"", + "prepublishOnly": "yarn run build" + }, + "devDependencies": {}, + "dependencies": { + "hardhat": "^2.22.10", + "@ethersproject/bignumber": "^5.7.0", + "@nomicfoundation/hardhat-ignition": "^0.15.5", + "@nomicfoundation/hardhat-verify": "^2.0.8", + "@openzeppelin/hardhat-upgrades": "^3.0.1", + "@openzeppelin/upgrades-core": "^1.32.2", + "ethers": "^6.8.1" + } +} diff --git a/packages/hre-extender-v2/src/extenders/extend-ethers.ts b/packages/hre-extender-v2/src/extenders/extend-ethers.ts new file mode 100644 index 00000000..e2b2b075 --- /dev/null +++ b/packages/hre-extender-v2/src/extenders/extend-ethers.ts @@ -0,0 +1,244 @@ +import { ethers } from "ethers"; +import { + DeployContractOptions, + FactoryOptions, + HardhatEthersHelpers, + Libraries, +} from "@nomicfoundation/hardhat-ethers/types"; +import { TenderlyPlugin } from "@tenderly/hardhat-tenderly"; +import { Artifact, HardhatRuntimeEnvironment } from "hardhat/types"; +import { TdlyContract } from "../types/TdlyContract"; +import { TdlyContractFactory } from "../types/TdlyContractFactory"; + +export function extendEthers(hre: HardhatRuntimeEnvironment): void { + if ( + "ethers" in hre && + hre.ethers !== undefined && + hre.ethers !== null && + "tenderly" in hre && + hre.tenderly !== undefined + ) { + Object.assign( + hre.ethers, + wrapEthers( + hre.ethers as unknown as typeof ethers & HardhatEthersHelpers, + hre.tenderly, + ) as unknown as typeof hre.ethers, + ); + } +} + +function wrapEthers( + nativeEthers: typeof ethers & HardhatEthersHelpers, + tenderly: TenderlyPlugin, +): typeof ethers & HardhatEthersHelpers { + // Factory + nativeEthers.getContractFactoryFromArtifact = + wrapGetContractFactoryFromArtifact( + nativeEthers.getContractFactoryFromArtifact, + tenderly, + ) as typeof nativeEthers.getContractFactoryFromArtifact; + nativeEthers.getContractFactory = wrapGetContractFactory( + nativeEthers.getContractFactory, + tenderly, + ) as typeof nativeEthers.getContractFactory; + + // Ethers's deployContract + nativeEthers.deployContract = wrapDeployContract( + nativeEthers.deployContract, + tenderly, + ) as typeof nativeEthers.deployContract; + + // Contract + nativeEthers.getContractAtFromArtifact = wrapGetContractAtFromArtifact( + nativeEthers.getContractAtFromArtifact, + tenderly, + ) as typeof nativeEthers.getContractAtFromArtifact; + nativeEthers.getContractAt = wrapGetContractAt( + nativeEthers.getContractAt, + tenderly, + ) as typeof nativeEthers.getContractAt; + + return nativeEthers; +} + +declare function getContractFactoryName( + name: string, + signerOrOptions?: ethers.Signer | FactoryOptions, +): Promise; + +declare function getContractFactoryABI( + abi: any[], + bytecode: ethers.BytesLike, + signer?: ethers.Signer, +): Promise; + +function wrapGetContractFactory( + func: typeof getContractFactoryName | typeof getContractFactoryABI, + tenderly: TenderlyPlugin, +): typeof getContractFactoryName | typeof getContractFactoryABI { + return async function ( + nameOrAbi: string | any[], + bytecodeOrFactoryOptions?: + | (ethers.Signer | FactoryOptions) + | ethers.BytesLike, + signer?: ethers.Signer, + ): Promise { + if (typeof nameOrAbi === "string") { + const contractFactory = await (func as typeof getContractFactoryName)( + nameOrAbi, + bytecodeOrFactoryOptions as ethers.Signer | FactoryOptions, + ); + + let libs; + const factoryOpts = bytecodeOrFactoryOptions as + | ethers.Signer + | FactoryOptions; + if (factoryOpts !== undefined && "libraries" in factoryOpts) { + libs = factoryOpts.libraries; + } + + return wrapContractFactory(contractFactory, tenderly, nameOrAbi, libs); + } + + return (func as typeof getContractFactoryABI)( + nameOrAbi, + bytecodeOrFactoryOptions as ethers.BytesLike, + signer, + ); + }; +} + +export declare function deployContract( + name: string, + signerOrOptions?: ethers.Signer | DeployContractOptions, +): Promise; + +function wrapDeployContract( + func: typeof deployContract, + tenderly: TenderlyPlugin, +): typeof deployContract { + return async function ( + name: string, + signerOrOptions?: ethers.Signer | DeployContractOptions, + ): Promise { + const contract = await func(name, signerOrOptions); + + let libraries; + if (signerOrOptions !== undefined && "libraries" in signerOrOptions) { + libraries = signerOrOptions.libraries; + } + + return new TdlyContract( + contract, + tenderly, + name, + libraries, + ) as unknown as ethers.Contract; + }; +} + +declare function getContractAt( + nameOrAbi: string | any[], + address: string, + signer?: ethers.Signer, +): Promise; + +function wrapGetContractAt( + func: typeof getContractAt, + tenderly: TenderlyPlugin, +): typeof getContractAt { + return async function ( + nameOrAbi: string | any[], + address: string, + signer?: ethers.Signer, + ): Promise { + if (typeof nameOrAbi === "string") { + const contract = await func(nameOrAbi, address, signer); + await tryToVerify(tenderly, nameOrAbi, contract); + + return contract; + } + + return func(nameOrAbi, address, signer); + }; +} + +declare function getContractFactoryFromArtifact( + artifact: Artifact, + signerOrOptions?: ethers.Signer | FactoryOptions, +): Promise; + +function wrapGetContractFactoryFromArtifact( + func: typeof getContractFactoryFromArtifact, + tenderly: TenderlyPlugin, +): typeof getContractFactoryFromArtifact { + return async function ( + artifact: Artifact, + signerOrOptions?: ethers.Signer | FactoryOptions, + ): Promise { + const contractFactory = await func(artifact, signerOrOptions); + + let libs; + const factoryOpts = signerOrOptions as ethers.Signer | FactoryOptions; + if (factoryOpts !== undefined && "libraries" in factoryOpts) { + libs = factoryOpts.libraries; + } + + return wrapContractFactory( + contractFactory, + tenderly, + artifact.contractName, + libs, + ); + }; +} + +declare function getContractAtFromArtifact( + artifact: Artifact, + address: string, + signer?: ethers.Signer, +): Promise; + +function wrapGetContractAtFromArtifact( + func: typeof getContractAtFromArtifact, + tenderly: TenderlyPlugin, +): typeof getContractAtFromArtifact { + return async function ( + artifact: Artifact, + address: string, + signer?: ethers.Signer, + ): Promise { + const contract = await func(artifact, address, signer); + await tryToVerify(tenderly, artifact.contractName, contract); + + return contract; + }; +} + +function wrapContractFactory( + contractFactory: ethers.ContractFactory, + tenderly: TenderlyPlugin, + name: string, + libraries?: Libraries, +): ethers.ContractFactory { + contractFactory = new TdlyContractFactory( + contractFactory, + tenderly, + name, + libraries, + ) as unknown as ethers.ContractFactory; + + return contractFactory; +} + +async function tryToVerify( + tenderly: TenderlyPlugin, + name: string, + contract: ethers.Contract, +) { + await tenderly.verify({ + name, + address: await contract.getAddress(), + }); +} diff --git a/packages/hre-extender-v2/src/extenders/extend-hardhat-deploy.ts b/packages/hre-extender-v2/src/extenders/extend-hardhat-deploy.ts new file mode 100644 index 00000000..5dc33f72 --- /dev/null +++ b/packages/hre-extender-v2/src/extenders/extend-hardhat-deploy.ts @@ -0,0 +1,55 @@ +import { + DeploymentsExtension, + DeployOptions, + DeployResult, +} from "hardhat-deploy/types"; + +import { TenderlyPlugin } from "@tenderly/hardhat-tenderly"; +import { HardhatRuntimeEnvironment } from "hardhat/types"; + +export function extendHardhatDeploy(hre: HardhatRuntimeEnvironment): void { + // ts-ignore is needed here because we want to avoid importing hardhat-deploy in order not to cause duplicated initialization of the .deployments field + if ( + "deployments" in hre && + // @ts-ignore + hre.deployments !== undefined && + "tenderly" in hre && + // @ts-ignore + hre.tenderly !== undefined + ) { + // @ts-ignore + hre.deployments = wrapHHDeployments(hre.deployments, hre.tenderly); + } +} + +export function wrapHHDeployments( + deployments: DeploymentsExtension, + tenderly: TenderlyPlugin, +) { + deployments.deploy = wrapDeploy(deployments.deploy, tenderly); + + return deployments; +} + +export declare function hhDeploy( + name: string, + options: DeployOptions, +): Promise; + +function wrapDeploy( + deployFunc: typeof hhDeploy, + tenderly: TenderlyPlugin, +): typeof hhDeploy { + return async function ( + name: string, + options: DeployOptions, + ): Promise { + const depResult = await deployFunc(name, options); + await tenderly.verify({ + name, + address: depResult.address, + }); + + return depResult; + }; +} diff --git a/packages/hre-extender-v2/src/extenders/extend-upgrades.ts b/packages/hre-extender-v2/src/extenders/extend-upgrades.ts new file mode 100644 index 00000000..4d82ff08 --- /dev/null +++ b/packages/hre-extender-v2/src/extenders/extend-upgrades.ts @@ -0,0 +1,160 @@ +import "@openzeppelin/hardhat-upgrades"; +import { Contract, ContractFactory, ethers } from "ethers"; +import { Artifact, HardhatRuntimeEnvironment } from "hardhat/types"; +import { + Libraries, + FactoryOptions, + HardhatEthersHelpers, + DeployContractOptions, +} from "@nomicfoundation/hardhat-ethers/types"; + +import { upgrades } from "hardhat"; +import { + ContractAddressOrInstance, + DeployBeaconProxyOptions, + DeployProxyOptions, +} from "@openzeppelin/hardhat-upgrades/dist/utils"; +import { TenderlyPlugin } from "@tenderly/hardhat-tenderly"; +import { logger } from "../logger"; +import { TdlyContractFactory } from "../types/TdlyContractFactory"; +import { TdlyProxyContract } from "../types/TdlyProxyContract"; + +export function extendUpgrades(hre: HardhatRuntimeEnvironment): void { + if ( + "upgrades" in hre && + hre.upgrades !== undefined && + hre.upgrades !== null && + "tenderly" in hre && + hre.tenderly !== undefined + ) { + logger.debug("Extending upgrades library"); + Object.assign( + hre.upgrades, + wrapUpgrades( + hre, + hre.upgrades as unknown as typeof upgrades & HardhatEthersHelpers, + hre.tenderly, + ) as unknown as typeof hre.upgrades, + ); + } +} + +export function wrapUpgrades( + hre: HardhatRuntimeEnvironment, + nativeUpgrades: typeof upgrades & HardhatEthersHelpers, + tenderly: TenderlyPlugin, +): typeof upgrades & HardhatEthersHelpers { + // Deploy Proxy + nativeUpgrades.deployProxy = wrapDeployProxy( + hre, + nativeUpgrades.deployProxy, + tenderly, + ) as typeof nativeUpgrades.deployProxy; + + // Deploy BeaconProxy + nativeUpgrades.deployBeaconProxy = wrapDeployBeaconProxy( + hre, + nativeUpgrades.deployBeaconProxy, + tenderly, + ) as typeof nativeUpgrades.deployBeaconProxy; + + return nativeUpgrades; +} + +export interface DeployFunction { + ( + ImplFactory: ContractFactory, + args?: unknown[], + opts?: DeployProxyOptions, + ): Promise; + (ImplFactory: ContractFactory, opts?: DeployProxyOptions): Promise; +} + +function wrapDeployProxy( + hre: HardhatRuntimeEnvironment, + func: DeployFunction, + tenderly: TenderlyPlugin, +): DeployFunction { + return async function ( + implFactory: ContractFactory, + argsOrOpts?: unknown[] | DeployProxyOptions, + opts?: DeployProxyOptions, + ) { + logger.debug("Calling ethers.Contract.deployProxy"); + let proxyContract; + if (opts !== undefined && opts !== null) { + proxyContract = await func(implFactory, argsOrOpts as unknown[], opts); + } else { + proxyContract = await func(implFactory, argsOrOpts as DeployProxyOptions); + } + + logger.debug("Returning TdlyProxyContract instance"); + return new TdlyProxyContract( + hre, + tenderly, + proxyContract, + ) as unknown as ethers.Contract; + }; +} + +export interface DeployBeaconProxyFunction { + ( + beacon: ContractAddressOrInstance, + attachTo: ContractFactory, + args?: unknown[], + opts?: DeployBeaconProxyOptions, + ): Promise; + ( + beacon: ContractAddressOrInstance, + attachTo: ContractFactory, + opts?: DeployBeaconProxyOptions, + ): Promise; +} + +function wrapDeployBeaconProxy( + hre: HardhatRuntimeEnvironment, + func: DeployBeaconProxyFunction, + tenderly: TenderlyPlugin, +): DeployBeaconProxyFunction { + return async function ( + beacon: ContractAddressOrInstance, + implFactory: ContractFactory, + argsOrOpts?: unknown[] | DeployBeaconProxyOptions, + opts?: DeployBeaconProxyOptions, + ): Promise { + if (isTdlyContractFactory(implFactory)) { + implFactory = implFactory.getNativeContractFactory(); + } + + let proxyContract; + if (opts !== undefined && opts !== null) { + proxyContract = await func( + beacon, + implFactory, + argsOrOpts as unknown[], + opts, + ); + } else { + proxyContract = await func( + beacon, + implFactory, + argsOrOpts as DeployBeaconProxyOptions, + ); + } + + return new TdlyProxyContract( + hre, + tenderly, + proxyContract, + ) as unknown as ethers.Contract; + }; +} + +function isTdlyContractFactory( + factory: ContractFactory | TdlyContractFactory, +): factory is TdlyContractFactory { + return ( + (factory as TdlyContractFactory).getNativeContractFactory !== undefined + ); +} + diff --git a/packages/hre-extender-v2/src/extenders/populate-hardhat-verify-config.ts b/packages/hre-extender-v2/src/extenders/populate-hardhat-verify-config.ts new file mode 100644 index 00000000..ef1584c8 --- /dev/null +++ b/packages/hre-extender-v2/src/extenders/populate-hardhat-verify-config.ts @@ -0,0 +1,125 @@ +// Returns true if the user has selected automatic population of hardhat-verify `etherscan` configuration through the TENDERLY_AUTOMATIC_POPULATE_HARDHAT_VERIFY_CONFIG env variable, +// and the network is some of the Tenderly networks. +import { HardhatRuntimeEnvironment, Network } from "hardhat/types"; +import { + isHttpNetworkConfig, + isTenderlyGatewayNetworkConfig, + isTenderlyNetworkConfig, +} from "./tenderly-network-resolver"; +import { getAccessToken } from "tenderly/utils/config"; +import { logger } from "../logger"; +import { getVnetTypeByEndpointId, VnetType } from "@tenderly/hardhat-tenderly/dist/tenderly/vnet-type"; +import * as URLComposer from "@tenderly/hardhat-tenderly/dist/utils/url-composer"; + +export function shouldPopulateHardhatVerifyConfig( + hre: HardhatRuntimeEnvironment, +): boolean { + return ( + // Must cover both since AUTOMATIC_POPULATE_HARDHAT_VERIFY_CONFIG is the legacy because we didn't use the TENDERLY_ prefix. + (process.env.TENDERLY_AUTOMATIC_POPULATE_HARDHAT_VERIFY_CONFIG === "true" || + process.env.AUTOMATIC_POPULATE_HARDHAT_VERIFY_CONFIG === "true") && + (isTenderlyNetworkConfig(hre.network.config) || + isTenderlyGatewayNetworkConfig(hre.network.config)) && + isHttpNetworkConfig(hre.network.config) + ); +} +// +// populateHardhatVerifyConfig will populate `hre.config.etherscan` configuration of the `@nomicfoundation/hardhat-verify` plugin. +// This function should import `@nomicfoundation/hardhat-verify` type declaration expansion of the `HardhatConfig`, but can't since there will be double overloading task error if the client application also uses `@nomicfoundation/hardhat-verify` plugin. +export async function populateHardhatVerifyConfig( + hre: HardhatRuntimeEnvironment, +): Promise { + if ( + (!isTenderlyNetworkConfig(hre.network.config) && + !isTenderlyGatewayNetworkConfig(hre.network.config)) || + !isHttpNetworkConfig(hre.network.config) + ) { + return; + } + + const accessKey = getAccessToken(); + if (accessKey === "") { + logger.error( + "Tenderly access key is not set. Please set TENDERLY_ACCESS_KEY environment variable.", + ); + return; + } + + if ( + (hre.config as any).etherscan === undefined || + (hre.config as any).etherscan === null + ) { + (hre.config as any).etherscan = { + apiKey: accessKey, + customChains: [], + }; + } + + if ( + isRecord((hre.config as any).etherscan.apiKey) && + (hre.config as any).etherscan.apiKey[hre.network.name] === undefined + ) { + (hre.config as any).etherscan.apiKey[hre.network.name] = accessKey; + } else if (typeof (hre.config as any).etherscan.apiKey === "string") { + (hre.config as any).etherscan.apiKey = accessKey; + } + + const chainId = await getChainId(hre.network); + + const endpointId = hre.network.config.url.split("/").pop(); + if (endpointId === undefined) { + throw new Error( + "Could not locate the UUID at the end of a Tenderly RPC URL.", + ); + } + + const vnetType = await getVnetTypeByEndpointId(hre, endpointId); + if (vnetType === VnetType.NULL_TYPE) { + throw new Error("Couldn't recognize VnetType from endpoint id."); + } + + (hre.config as any).etherscan.customChains.push({ + network: hre.network.name, + chainId, + urls: { + apiURL: URLComposer.composeApiURL(hre, endpointId, chainId, vnetType), + browserURL: URLComposer.composeBrowserURL( + hre, + endpointId, + chainId, + vnetType, + ), + }, + }); +} + +export async function findEtherscanConfig( + hre: HardhatRuntimeEnvironment, +): Promise { + if ((hre.config as any).etherscan === undefined) { + return undefined; + } + if ((hre.config as any).etherscan.customChains === undefined) { + return undefined; + } + + return ((hre.config as any).etherscan.customChains as any[]).find( + (chainConfig) => { + return chainConfig.network === hre.network.name; + }, + ); +} + + +function isRecord(value: any): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +async function getChainId(network: Network): Promise { + if (network.config.chainId !== undefined && network.config.chainId !== null) { + return network.config.chainId; + } + + return Number(await network.provider.send("eth_chainId", [])); +} + diff --git a/packages/hre-extender-v2/src/extenders/tenderly-network-resolver.ts b/packages/hre-extender-v2/src/extenders/tenderly-network-resolver.ts new file mode 100644 index 00000000..c54bfdba --- /dev/null +++ b/packages/hre-extender-v2/src/extenders/tenderly-network-resolver.ts @@ -0,0 +1,40 @@ +// isTenderlyNetworkConfig checks if a network belongs to tenderly by checking the rpc_url. +// This is done so the user can put custom network names in the hardhat config and still use them with tenderly. +// This is also done so the user can use multiple tenderly networks in their hardhat config file. +import { HttpNetworkConfig, NetworkConfig } from "hardhat/types"; + +export const isTenderlyNetworkConfig = (nw: NetworkConfig): boolean => { + if (nw === undefined || nw === null) { + return false; + } + if (!isHttpNetworkConfig(nw)) { + return false; + } + + // The network belongs to tenderly if the rpc_url is one of the following: + // - https://rpc.vnet.tenderly.co/devnet/... + // - https://.rpc.tenderly.co/... + // - https://virtual..rpc.tenderly.co/... + // - https://rpc.tenderly.co/... + const regex = + /^https?:\/\/(?:rpc\.vnet\.tenderly\.co\/devnet\/|(?:[\w-]+\.rpc|rpc)\.tenderly\.co\/|virtual\.[\w-]+\.rpc\.tenderly\.co\/).*$/; + return regex.test(nw.url); +}; + +export function isTenderlyGatewayNetworkConfig(nw: NetworkConfig): boolean { + if (nw === undefined || nw === null) { + return false; + } + if (!isHttpNetworkConfig(nw)) { + return false; + } + + const regex = /^https?:\/\/[\w-]+\.gateway\.tenderly\.co\/.*$/; + return regex.test(nw.url); +} + +export function isHttpNetworkConfig( + config: NetworkConfig, +): config is HttpNetworkConfig { + return (config as HttpNetworkConfig).url !== undefined; +} diff --git a/packages/hre-extender-v2/src/index.ts b/packages/hre-extender-v2/src/index.ts new file mode 100644 index 00000000..3c1aa314 --- /dev/null +++ b/packages/hre-extender-v2/src/index.ts @@ -0,0 +1,6 @@ +import { logger } from "./logger"; + +logger.settings.minLevel = 4; + +export * from "@tenderly/hardhat-tenderly"; +export { setup } from "./setup"; diff --git a/packages/hre-extender-v2/src/logger.ts b/packages/hre-extender-v2/src/logger.ts new file mode 100644 index 00000000..eecfd5c1 --- /dev/null +++ b/packages/hre-extender-v2/src/logger.ts @@ -0,0 +1,6 @@ +import { Logger } from "tslog"; + +export const logger = new Logger({ + prettyLogTemplate: "{{dateIsoStr}} {{logLevelName}} {{name}} =>", + name: "HreExtender", +}); diff --git a/packages/hre-extender-v2/src/setup.ts b/packages/hre-extender-v2/src/setup.ts new file mode 100644 index 00000000..d22ad5ae --- /dev/null +++ b/packages/hre-extender-v2/src/setup.ts @@ -0,0 +1,185 @@ +import "@openzeppelin/hardhat-upgrades"; +import { lazyObject } from "hardhat/plugins"; +import { extendConfig, extendEnvironment } from "hardhat/config"; +import { + HardhatRuntimeEnvironment, + HttpNetworkConfig, + HardhatConfig, +} from "hardhat/types"; +import { logger as serviceLogger } from "tenderly/utils/logger"; +import { + CHAIN_ID_NETWORK_NAME_MAP, + NETWORK_NAME_CHAIN_ID_MAP, PLUGIN_NAME, + TENDERLY_JSON_RPC_BASE_URL, +} from "tenderly/common/constants"; + +import{ TenderlyNetwork as TenderlyNetworkInterface } from "tenderly/types"; + +import { logger } from "./logger"; +import { TenderlyService } from "tenderly"; +import { Tenderly, TenderlyNetwork } from "@tenderly/hardhat-tenderly"; +import { extendEthers } from "./extenders/extend-ethers"; +import { extendUpgrades } from "./extenders/extend-upgrades"; +import { extendHardhatDeploy } from "./extenders/extend-hardhat-deploy"; +import { isTenderlyNetworkConfig } from "./extenders/tenderly-network-resolver"; +import { + findEtherscanConfig, + populateHardhatVerifyConfig, + shouldPopulateHardhatVerifyConfig, +} from "./extenders/populate-hardhat-verify-config"; + +const tenderlyService = new TenderlyService(PLUGIN_NAME); + +export function setup() { + // set to loggers to error level by default + logger.settings.minLevel = 4; + serviceLogger.settings.minLevel = 4; + + extendEnvironment(async (hre: HardhatRuntimeEnvironment) => { + hre.tenderly = lazyObject(() => new Tenderly(hre)); + + if (hre.hardhatArguments.verbose) { + logger.settings.minLevel = 1; // trace level + serviceLogger.settings.minLevel = 1; // trace level + } + logger.info( + `Setting up hardhat-tenderly plugin. Log level of hardhat tenderly plugin set to: ${logger.settings.minLevel}`, + ); + // serviceLogger is used here just for initialization, nothing else, it will be used in TenderlyService.ts + serviceLogger.info( + `Log level of tenderly service set to: ${serviceLogger.settings.minLevel}`, + ); + + const pjson = require("../package.json"); + logger.info("@tenderly/hardhat-tenderly version:", pjson.version); + + logger.info("Tenderly running configuration: ", { + username: hre.config.tenderly?.username, + project: hre.config.tenderly?.project, + automaticVerification: process.env.AUTOMATIC_VERIFICATION_ENABLED, + privateVerification: hre.config.tenderly?.privateVerification, + networkName: hre.network.name, + }); + + extendProvider(hre); + populateNetworks(); + if (process.env.AUTOMATIC_VERIFICATION_ENABLED === "true" || process.env.TENDERLY_AUTOMATIC_VERIFICATION === "true") { + logger.debug( + "Automatic verification is enabled, proceeding to extend ethers library.", + ); + extendEthers(hre); + extendUpgrades(hre); + extendHardhatDeploy(hre); + logger.debug("Wrapping ethers library finished."); + } + + if (shouldPopulateHardhatVerifyConfig(hre)) { + logger.info( + "Automatic population of hardhat-verify `etherscan` configuration is enabled.", + ); + // If the config already exists, we should not overwrite it, either remove it or turn off automatic population. + const etherscanConfig = await findEtherscanConfig(hre); + if (etherscanConfig !== undefined) { + throw new Error( + `Hardhat-verify's 'etherscan' configuration with network '${ + hre.network.name + }' is already populated. Please remove the following configuration:\n${JSON.stringify( + etherscanConfig, + null, + 2, + )}\nOr set 'TENDERLY_AUTOMATIC_POPULATE_HARDHAT_VERIFY_CONFIG' environment variable to 'false'`, + ); + } + await populateHardhatVerifyConfig(hre); + } + + logger.debug("Setup finished."); + }); +} + +extendEnvironment((hre: HardhatRuntimeEnvironment) => { + hre.tenderly = lazyObject(() => new Tenderly(hre)); + extendProvider(hre); + populateNetworks(); +}); + +extendConfig((resolvedConfig: HardhatConfig) => { + resolvedConfig.networks.tenderly = { + ...resolvedConfig.networks.tenderly, + }; +}); + +const extendProvider = (hre: HardhatRuntimeEnvironment): void => { + if (!isTenderlyNetworkConfig(hre.network.config)) { + logger.info( + `Used network is not 'tenderly' so there is no extending of the provider.`, + ); + return; + } + + if ("url" in hre.network.config && hre.network.config.url !== undefined) { + if (hre.network.config.url.includes("devnet")) { + const devnetID = hre.network.config.url.split("/").pop(); + hre.tenderly.network().setDevnetID(devnetID); + logger.info( + `There is a devnet url in the '${hre.network.name}' network`, + { devnetID }, + ); + return; + } + const forkID = hre.network.config.url.split("/").pop(); + hre.tenderly.network().setFork(forkID); + logger.info(`There is a fork url in the 'tenderly' network`, { forkID }); + return; + } + + const tenderlyNetwork = new TenderlyNetwork(hre); + tenderlyNetwork + .initializeFork() + .then(async (_) => { + hre.tenderly.setNetwork(tenderlyNetwork); + const forkID = await hre.tenderly.network().getForkID(); + ( + hre.network.config as HttpNetworkConfig + ).url = `${TENDERLY_JSON_RPC_BASE_URL}/fork/${forkID ?? ""}`; + // hre.ethers.provider = new hre.ethers.BrowserProvider(hre.tenderly.network()); + }) + .catch((_) => { + logger.error( + `Error happened while trying to initialize fork ${PLUGIN_NAME}. Check your tenderly configuration`, + ); + }); +}; + +const populateNetworks = (): void => { + tenderlyService + .getNetworks() + .then((networks: TenderlyNetworkInterface[]) => { + let network: TenderlyNetworkInterface; + let slug: string; + for (network of networks) { + NETWORK_NAME_CHAIN_ID_MAP[network.slug] = network.ethereum_network_id; + + if (network?.metadata?.slug !== undefined) { + NETWORK_NAME_CHAIN_ID_MAP[network.metadata.slug] = + network.ethereum_network_id; + } + + CHAIN_ID_NETWORK_NAME_MAP[network.ethereum_network_id] = network.slug; + + for (slug of network.metadata.secondary_slugs) { + NETWORK_NAME_CHAIN_ID_MAP[slug] = network.ethereum_network_id; + } + } + logger.silly( + "Obtained supported public networks: ", + NETWORK_NAME_CHAIN_ID_MAP, + ); + }) + .catch((_) => { + logger.error("Error encountered while fetching public networks"); + }); +}; + + + diff --git a/packages/hre-extender-v2/src/types/ContractByName.ts b/packages/hre-extender-v2/src/types/ContractByName.ts new file mode 100644 index 00000000..47b05a7c --- /dev/null +++ b/packages/hre-extender-v2/src/types/ContractByName.ts @@ -0,0 +1,8 @@ +import { Libraries } from "@nomicfoundation/hardhat-ethers/types"; + +export interface ContractByName { + name: string; + address: string; + network?: string; + libraries?: Libraries; +} diff --git a/packages/hre-extender-v2/src/types/TdlyContract.ts b/packages/hre-extender-v2/src/types/TdlyContract.ts new file mode 100644 index 00000000..5242d167 --- /dev/null +++ b/packages/hre-extender-v2/src/types/TdlyContract.ts @@ -0,0 +1,133 @@ +import { Contract, ethers } from "ethers"; +import { BigNumber } from "@ethersproject/bignumber"; +import { Libraries } from "@nomicfoundation/hardhat-ethers/types"; + +import { TenderlyPlugin } from "@tenderly/hardhat-tenderly"; +import { ContractByName } from "./ContractByName"; + +export class TdlyContract { + [key: string]: any; + + private readonly contractName: string; + private nativeContract: ethers.Contract; + private tenderly: TenderlyPlugin; + private readonly libraries: Libraries | undefined; + + constructor( + nativeContract: ethers.Contract, + tenderly: TenderlyPlugin, + contractName: string, + libs?: Libraries, + ) { + this.contractName = contractName; + this.nativeContract = nativeContract; + this.tenderly = tenderly; + this.libraries = libs; + + Object.keys(nativeContract).forEach((key) => { + if (this[key] !== undefined) { + return; + } + + if (key === "deploymentTransaction") { + const deploymentTransaction = nativeContract[key](); + if ( + deploymentTransaction === undefined || + deploymentTransaction === null + ) { + return; + } + + const wait = deploymentTransaction.wait; + + deploymentTransaction.wait = async ( + confirmations?: number | undefined, + ): Promise => { + const receipt = await wait(confirmations); + if (receipt === undefined || receipt === null) { + return null; + } + + if ( + receipt.contractAddress === undefined || + receipt.contractAddress === null + ) { + return receipt; + } + await this._tdlyVerify(receipt.contractAddress); + + return receipt; + }; + } + + this[key] = nativeContract[key]; + }); + } + + public async waitForDeployment(): Promise { + const contract = await this.nativeContract.waitForDeployment(); + const deploymentTransaction = this.nativeContract.deploymentTransaction(); + if (deploymentTransaction !== undefined && deploymentTransaction !== null) { + await this._tdlyVerify(await contract.getAddress()); + } + + return contract; + } + + public deploymentTransaction(): null | ethers.ContractTransactionResponse { + return this.nativeContract.deploymentTransaction(); + } + + public async getAddress(): Promise { + return this.nativeContract.getAddress(); + } + + private async _tdlyVerify(address: string) { + const contPair: ContractByName = { + name: this.contractName, + address, + }; + if (this.libraries !== undefined && this.libraries !== null) { + contPair.libraries = this.libraries; + } + + await this.tenderly.persistArtifacts(contPair); + await this.tenderly.verify(contPair); + } +} + +export interface TransactionReceipt { + to: string; + from: string; + contractAddress: string; + transactionIndex: number; + root?: string; + gasUsed: BigNumber; + logsBloom: string; + blockHash: string; + transactionHash: string; + logs: Log[]; + blockNumber: number; + confirmations: number; + cumulativeGasUsed: BigNumber; + effectiveGasPrice: BigNumber; + byzantium: boolean; + type: number; + status?: number; +} + +export interface Log { + blockNumber: number; + blockHash: string; + transactionIndex: number; + + removed: boolean; + + address: string; + data: string; + + topics: string[]; + + transactionHash: string; + logIndex: number; +} diff --git a/packages/hre-extender-v2/src/types/TdlyContractFactory.ts b/packages/hre-extender-v2/src/types/TdlyContractFactory.ts new file mode 100644 index 00000000..e6c73d6d --- /dev/null +++ b/packages/hre-extender-v2/src/types/TdlyContractFactory.ts @@ -0,0 +1,98 @@ +import { Contract, ContractFactory, Signer } from "ethers"; +import { Libraries } from "@nomicfoundation/hardhat-ethers/types"; + +import { TenderlyPlugin } from "@tenderly/hardhat-tenderly"; +import { TdlyContract } from "./TdlyContract"; + +export class TdlyContractFactory { + [key: string]: any; + + private readonly contractName: string; + private readonly libs: Libraries | undefined; + private readonly nativeContractFactory: ContractFactory; + private readonly tenderly: TenderlyPlugin; + + constructor( + nativeContractFactory: ContractFactory, + tenderly: TenderlyPlugin, + contractName: string, + libs?: Libraries, + ) { + this.nativeContractFactory = nativeContractFactory; + this.tenderly = tenderly; + this.contractName = contractName; + this.libs = libs; + + for (const attribute in Object.assign(nativeContractFactory)) { + if (this[attribute] !== undefined) { + continue; + } + this[attribute] = (nativeContractFactory as any)[attribute]; + } + + classFunctions(nativeContractFactory).forEach((funcName: string) => { + if (this[funcName] !== undefined) { + return; + } + this[funcName] = (nativeContractFactory as any)[funcName]; + }); + } + + public async deploy(...args: any[]): Promise { + const contract = await this.nativeContractFactory.deploy(...args); + + return new TdlyContract( + contract as Contract, + this.tenderly, + this.contractName, + this.libs, + ) as unknown as Contract; + } + + public connect(signer: Signer) { + const contractFactory = this.nativeContractFactory.connect(signer); + + return new TdlyContractFactory( + contractFactory, + this.tenderly, + this.contractName, + this.libs, + ); + } + + public getLibs(): Libraries | undefined { + return this.libs; + } + + public getContractName(): string { + return this.contractName; + } + + public getNativeContractFactory(): ContractFactory { + return this.nativeContractFactory; + } +} + +const classFunctions = (x: any) => + distinctDeepFunctions(x).filter( + (name: string) => name !== "constructor" && name.indexOf("__") === -1, + ); + +const distinctDeepFunctions = (x: any) => Array.from(new Set(deepFunctions(x))); + +const deepFunctions = (x: any): string[] => { + if (x && x !== Object.prototype) { + return Object.getOwnPropertyNames(x) + .filter( + (name: string) => isGetter(x, name) !== null || isFunction(x, name), + ) + .concat(deepFunctions(Object.getPrototypeOf(x)) ?? []); + } + return []; +}; + +const isGetter = (x: any, name: string): any => + ((Object.getOwnPropertyDescriptor(x, name) !== null || {}) as any).get; + +const isFunction = (x: any, name: string): boolean => + typeof x[name] === "function"; diff --git a/packages/hre-extender-v2/src/types/TdlyProxyContract.ts b/packages/hre-extender-v2/src/types/TdlyProxyContract.ts new file mode 100644 index 00000000..67b5dc70 --- /dev/null +++ b/packages/hre-extender-v2/src/types/TdlyProxyContract.ts @@ -0,0 +1,45 @@ +import { ethers } from "ethers"; + +import { HardhatRuntimeEnvironment } from "hardhat/types"; +import { TenderlyPlugin } from "@tenderly/hardhat-tenderly"; + +export class TdlyProxyContract { + [key: string]: any; + + private readonly hre: HardhatRuntimeEnvironment; + private tenderly: TenderlyPlugin; + private proxyContract: ethers.Contract; + + constructor( + hre: HardhatRuntimeEnvironment, + tenderly: TenderlyPlugin, + proxyContract: ethers.Contract, + ) { + this.hre = hre; + this.tenderly = tenderly; + this.proxyContract = proxyContract; + } + + public async waitForDeployment(): Promise { + const proxyContract = await this.proxyContract.waitForDeployment(); + const deploymentTransaction = this.proxyContract.deploymentTransaction(); + if (deploymentTransaction !== undefined && deploymentTransaction !== null) { + // verify:verify task should verify the proxy (regardless of proxy type), implementation and all the related contracts. + // logger.debug("Running hardhat-verify's verify task "); + await this.hre.run("verify:verify", { + address: await proxyContract.getAddress(), + constructorArguments: [], + }); + } + + return proxyContract; + } + + public deploymentTransaction(): null | ethers.ContractTransactionResponse { + return this.proxyContract.deploymentTransaction(); + } + + public async getAddress(): Promise { + return this.nativeContract.getAddress(); + } +} diff --git a/packages/hre-extender-v2/tsconfig.json b/packages/hre-extender-v2/tsconfig.json new file mode 100644 index 00000000..0d61af11 --- /dev/null +++ b/packages/hre-extender-v2/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../config/typescript/tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "composite": true + }, + "include": ["src/**/*.ts"], + "exclude": ["./dist", "./node_modules"], + "references": [ + { + "path": "../tenderly-hardhat" + }, + { + "path": "../tenderly-core" + } + ] +}