UNPKG

hardhat-deploy

Version:

Hardhat Plugin For Replicable Deployments And Tests

468 lines (429 loc) 14.1 kB
/* eslint-disable @typescript-eslint/no-explicit-any */ import fs from 'fs'; import axios from 'axios'; import {stringify} from 'neoqs/legacy'; import path from 'path'; import {defaultAbiCoder, ParamType} from '@ethersproject/abi'; import {HardhatRuntimeEnvironment} from 'hardhat/types'; import chalk from 'chalk'; import matchAll from 'match-all'; const defaultEndpoint = `https://api.etherscan.io/v2/api`; function log(...args: any[]) { console.log(...args); } function logError(...args: any[]) { console.log(chalk.red(...args)); } function logInfo(...args: any[]) { console.log(chalk.yellow(...args)); } function logSuccess(...args: any[]) { console.log(chalk.green(...args)); } function sleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } function writeRequestIfRequested( write: boolean, networkName: string, name: string, request: string, // eslint-disable-next-line @typescript-eslint/no-unused-vars postData: any ) { if (write) { try { fs.mkdirSync('etherscan_requests'); } catch (e) {} const folder = `etherscan_requests/${networkName}`; try { fs.mkdirSync(folder); } catch (e) {} fs.writeFileSync(`${folder}/${name}.formdata`, request); fs.writeFileSync(`${folder}/${name}.json`, JSON.stringify(postData)); fs.writeFileSync( `${folder}/${name}_multi-source.json`, postData.sourceCode ); } } function extractOneLicenseFromSourceFile(source: string): string | undefined { const licenses = extractLicenseFromSources(source); if (licenses.length === 0) { return undefined; } return licenses[0]; // TODO error out on multiple SPDX ? } function extractLicenseFromSources(metadata: string): string[] { const regex = /\/\/\s*\t*SPDX-License-Identifier:\s*\t*(.*?)[\s\\]/g; const matches = matchAll(metadata, regex).toArray(); const licensesFound: {[license: string]: boolean} = {}; const licenses = []; if (matches) { for (const match of matches) { if (!licensesFound[match]) { licensesFound[match] = true; licenses.push(match); } } } return licenses; } function getLicenseType(license: string): undefined | number { const licenseType = (() => { if (license === 'None') { return 1; } if (license === 'UNLICENSED') { return 2; } if (license === 'MIT') { return 3; } if (license === 'GPL-2.0') { return 4; } if (license === 'GPL-3.0') { return 5; } if (license === 'LGPL-2.1') { return 6; } if (license === 'LGPL-3.0') { return 7; } if (license === 'BSD-2-Clause') { return 8; } if (license === 'BSD-3-Clause') { return 9; } if (license === 'MPL-2.0') { return 10; } if (license === 'OSL-3.0') { return 11; } if (license === 'Apache-2.0') { return 12; } if (license === 'AGPL-3.0') { return 13; } if (license === 'BUSL-1.1') { return 14; } })(); return licenseType; } export async function submitSources( hre: HardhatRuntimeEnvironment, solcInputsPath: string, config?: { contractName?: string; etherscanApiKey?: string; license?: string; fallbackOnSolcInput?: boolean; forceLicense?: boolean; sleepBetween?: boolean; apiUrl?: string; writePostData?: boolean; } ): Promise<void> { config = config || {}; const fallbackOnSolcInput = config.fallbackOnSolcInput; const licenseOption = config.license; const forceLicense = config.forceLicense; const etherscanApiKey = config.etherscanApiKey; const sleepBetween = config.sleepBetween; const all = await hre.deployments.all(); const networkName = hre.network.name; const chainId = await hre.getChainId(); let endpoint = config.apiUrl; if (!endpoint) { endpoint = defaultEndpoint; } async function submit(name: string, useSolcInput?: boolean) { const deployment = all[name]; const {address, metadata: metadataString} = deployment; const abiResponse = await axios.get( `${endpoint}?chainid=${chainId}&module=contract&action=getabi&address=${address}&apikey=${etherscanApiKey}` ); const {data: abiData} = abiResponse; let contractABI; if (abiData.status !== '0') { try { contractABI = JSON.parse(abiData.result); } catch (e) { logError(e); return; } } if (contractABI && contractABI !== '') { log(`already verified: ${name} (${address}), skipping.`); return; } if (!metadataString) { logError( `Contract ${name} was deployed without saving metadata. Cannot submit to etherscan, skipping.` ); return; } const metadata = JSON.parse(metadataString); const compilationTarget = metadata.settings?.compilationTarget; let contractFilepath; let contractName; if (compilationTarget) { contractFilepath = Object.keys(compilationTarget)[0]; contractName = compilationTarget[contractFilepath]; } if (!contractFilepath || !contractName) { return logError( `Failed to extract contract fully qualified name from metadata.settings.compilationTarget for ${name}. Skipping.` ); } const contractNamePath = `${contractFilepath}:${contractName}`; const contractSourceFile = metadata.sources[contractFilepath].content; const sourceLicenseType = extractOneLicenseFromSourceFile(contractSourceFile); let license = licenseOption; if (!sourceLicenseType) { if (!license) { return logError( `no license speccified in the source code for ${name} (${contractNamePath}), Please use option --license <SPDX>` ); } } else { if (license && license !== sourceLicenseType) { if (!forceLicense) { return logError( `mismatch for --license option (${licenseOption}) and the one specified in the source code for ${name}.\nLicenses found in source : ${sourceLicenseType}\nYou can use option --force-license to force option --license` ); } } else { license = sourceLicenseType; if (!getLicenseType(license)) { return logError( `license :"${license}" found in source code for ${name} (${contractNamePath}) but this license is not supported by etherscan, list of supported license can be found here : https://etherscan.io/contract-license-types . This tool expect the SPDX id, except for "None" and "UNLICENSED"` ); } } } const licenseType = getLicenseType(license); if (!licenseType) { return logError( `license :"${license}" not supported by etherscan, list of supported license can be found here : https://etherscan.io/contract-license-types . This tool expect the SPDX id, except for "None" and "UNLICENSED"` ); } let solcInput: { language: string; settings: any; sources: Record<string, {content: string}>; }; if (useSolcInput) { const solcInputHash = deployment.solcInputHash; let solcInputStringFromDeployment: string | undefined; try { solcInputStringFromDeployment = fs .readFileSync(path.join(solcInputsPath, solcInputHash + '.json')) .toString(); } catch (e) {} if (!solcInputStringFromDeployment) { logError( `Contract ${name} was deployed without saving solcInput. Cannot submit to etherscan, skipping.` ); return; } solcInput = JSON.parse(solcInputStringFromDeployment); } else { const settings = {...metadata.settings}; delete settings.compilationTarget; solcInput = { language: metadata.language, settings, sources: {}, }; for (const sourcePath of Object.keys(metadata.sources)) { const source = metadata.sources[sourcePath]; // only content as this fails otherwise solcInput.sources[sourcePath] = { content: source.content, }; } } // Adding Libraries .... if (deployment.libraries) { const settings = solcInput.settings; settings.libraries = settings.libraries || {}; for (const libraryName of Object.keys(deployment.libraries)) { if (!settings.libraries[contractNamePath]) { settings.libraries[contractNamePath] = {}; } settings.libraries[contractNamePath][libraryName] = deployment.libraries[libraryName]; } } const solcInputString = JSON.stringify(solcInput); logInfo(`verifying ${name} (${address}) ...`); let constructorArguements: string | undefined; if (deployment.args) { const constructor: {inputs: ParamType[]} = deployment.abi.find( (v) => v.type === 'constructor' ); if (constructor) { constructorArguements = defaultAbiCoder .encode(constructor.inputs, deployment.args) .slice(2); } } else { logInfo(`no args found, assuming empty constructor...`); } const postData: { [fieldName: string]: string | number | void | undefined; // TODO type } = { apikey: etherscanApiKey, module: 'contract', action: 'verifysourcecode', contractaddress: address, sourceCode: solcInputString, codeformat: 'solidity-standard-json-input', contractname: contractNamePath, compilerversion: `v${metadata.compiler.version}`, // see http://etherscan.io/solcversions for list of support versions constructorArguements, // note the spelling mistake by etherscan licenseType, }; const formDataAsString = stringify(postData); const submissionResponse = await axios.request({ url: `${endpoint}?chainid=${chainId}`, method: 'POST', headers: {'content-type': 'application/x-www-form-urlencoded'}, data: formDataAsString, }); const {data: submissionData} = submissionResponse; let guid: string; if (submissionData.status === '1') { guid = submissionData.result; } else { logError( `contract ${name} failed to submit : "${submissionData.message}" : "${submissionData.result}"`, submissionData ); writeRequestIfRequested( config?.writePostData || false, networkName, name, formDataAsString, postData ); return; } if (!guid) { logError(`contract submission for ${name} failed to return a guid`); writeRequestIfRequested( config?.writePostData || false, networkName, name, formDataAsString, postData ); return; } async function checkStatus(): Promise<string | undefined> { // TODO while loop and delay : // console.log(`checking status for ${name} (${address})...`); const statusResponse = await axios.get( `${endpoint}?chainid=${chainId}&apikey=${etherscanApiKey}`, { params: { guid, module: 'contract', action: 'checkverifystatus', }, } ); const {data: statusData} = statusResponse; // blockscout seems to return status == 1 in case of failure // so we check string first if (statusData.result === 'Pending in queue') { return undefined; } if (statusData.result !== 'Fail - Unable to verify') { if (statusData.status === '1') { // console.log(statusData); return 'success'; } } logError( `Failed to verify contract ${name}: ${statusData.message}, ${statusData.result}` ); logError( JSON.stringify( { apikey: 'XXXXXX', module: 'contract', action: 'verifysourcecode', contractaddress: address, sourceCode: '...', codeformat: 'solidity-standard-json-input', contractname: contractNamePath, compilerversion: `v${metadata.compiler.version}`, // see http://etherscan.io/solcversions for list of support versions constructorArguements, licenseType, }, null, ' ' ) ); // logError(JSON.stringify(postData, null, " ")); // logInfo(postData.sourceCode); return 'failure'; } logInfo('waiting for result...'); let result; while (!result) { await new Promise((resolve) => setTimeout(resolve, 10 * 1000)); result = await checkStatus(); } if (result === 'success') { logSuccess(` => contract ${name} is now verified`); } if (result === 'failure') { if (!useSolcInput && fallbackOnSolcInput) { logInfo( 'Falling back on solcInput. etherscan seems to sometime require full solc-input with all source files, even though this should not be needed. See https://github.com/ethereum/solidity/issues/9573' ); await submit(name, true); } else { writeRequestIfRequested( config?.writePostData || false, networkName, name, formDataAsString, postData ); logInfo( 'Etherscan sometime fails to verify when only metadata sources are given. See https://github.com/ethereum/solidity/issues/9573. You can add the option --solc-input to try with full solc-input sources. This will include all contract source in the etherscan result, even the one not relevant to the contract being verified' ); } } else { writeRequestIfRequested( config?.writePostData || false, networkName, name, formDataAsString, postData ); } } if (config.contractName) { await submit(config.contractName); } else { for (const name of Object.keys(all)) { await submit(name); if (sleepBetween) { // sleep between each verification so we don't exceed the API rate limit await sleep(500); } } } }