diff --git a/hardhat.config.ts b/hardhat.config.ts index e545f203c..2c4a08209 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -28,7 +28,7 @@ const SKIP_LOAD = process.env.SKIP_LOAD === 'true' function loadTasks() { require('./tasks/gre.ts') - ;['contracts', 'misc', 'deployment', 'actions'].forEach((folder) => { + ;['contracts', 'misc', 'deployment', 'actions', 'verify'].forEach((folder) => { const tasksPath = path.join(__dirname, 'tasks', folder) fs.readdirSync(tasksPath) .filter((pth) => pth.includes('.ts')) @@ -103,6 +103,9 @@ const config: HardhatUserConfig = { enabled: true, runs: 200, }, + metadata: { + useLiteralContent: true, + }, outputSelection: { '*': { '*': ['storageLayout'], diff --git a/package.json b/package.json index 8cab599c9..6fba87cbd 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "eslint-plugin-no-only-tests": "^2.4.0", "eslint-plugin-prettier": "^3.4.0", "ethereum-waffle": "^3.3.0", + "form-data": "^4.0.0", "graphql-tag": "^2.12.4", "hardhat": "^2.9.5", "hardhat-abi-exporter": "^2.2.0", diff --git a/tasks/verify/sourcify.ts b/tasks/verify/sourcify.ts new file mode 100644 index 000000000..a872884bc --- /dev/null +++ b/tasks/verify/sourcify.ts @@ -0,0 +1,77 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import axios from 'axios' +import FormData from 'form-data' +import { HardhatRuntimeEnvironment } from 'hardhat/types' +import { Readable } from 'stream' + +// Inspired by: +// - https://github.com/wighawag/hardhat-deploy/blob/9c8cd433a37188e793181b727222e2d22aef34b0/src/sourcify.ts +// - https://github.com/zoey-t/hardhat-sourcify/blob/26f10a08eb6cf97700c78989bf42b009c9cb3275/src/sourcify.ts +export async function submitSourcesToSourcify( + hre: HardhatRuntimeEnvironment, + contract: { + source: string + name: string + address: string + fqn: string + }, +): Promise { + const chainId = hre.network.config.chainId + const sourcifyUrl = 'https://sourcify.dev/server/' + + // Get contract metadata + const contractBuildInfo = await hre.artifacts.getBuildInfo(contract.fqn) + const contractMetadata = ( + contractBuildInfo.output.contracts[contract.source][contract.name] as any + ).metadata + + if (contractMetadata === undefined) { + console.error( + `Contract ${contract.name} was deployed without saving metadata. Cannot submit to sourcify, skipping.`, + ) + return + } + + // Check if contract already verified + try { + const checkResponse = await axios.get( + `${sourcifyUrl}checkByAddresses?addresses=${contract.address.toLowerCase()}&chainIds=${chainId}`, + ) + const { data: checkData } = checkResponse + if (checkData[0].status === 'perfect') { + console.log(`already verified: ${contract.name} (${contract.address}), skipping.`) + return + } + } catch (e) { + console.error(((e as any).response && JSON.stringify((e as any).response.data)) || e) + } + + console.log(`verifying ${contract.name} (${contract.address} on chain ${chainId}) ...`) + + // Build form data + const formData = new FormData() + formData.append('address', contract.address) + formData.append('chain', chainId) + + const fileStream = new Readable() + fileStream.push(contractMetadata) + fileStream.push(null) + formData.append('files', fileStream) + + // Verify contract + try { + const submissionResponse = await axios.post(sourcifyUrl, formData, { + headers: formData.getHeaders(), + }) + const { status } = submissionResponse.data.result[0] + if (status === 'perfect') { + console.log(` => contract ${contract.name} is now verified`) + } else if (status === 'partial') { + console.log(` => contract ${contract.name} is partially verified`) + } else { + console.error(` => contract ${contract.name} is not verified`) + } + } catch (e) { + console.error(((e as any).response && JSON.stringify((e as any).response.data)) || e) + } +} diff --git a/tasks/verify/verify.ts b/tasks/verify/verify.ts new file mode 100644 index 000000000..6139aadda --- /dev/null +++ b/tasks/verify/verify.ts @@ -0,0 +1,29 @@ +import { task } from 'hardhat/config' +import * as types from 'hardhat/internal/core/params/argumentTypes' +import { submitSourcesToSourcify } from './sourcify' +import { isFullyQualifiedName, parseFullyQualifiedName } from 'hardhat/utils/contract-names' +import { TASK_COMPILE } from 'hardhat/builtin-tasks/task-names' +import fs from 'fs' + +task('sourcify', 'Verifies contract on sourcify') + .addPositionalParam('address', 'Address of the smart contract to verify', undefined, types.string) + .addParam('contract', 'Fully qualified name of the contract to verify.', undefined, types.string) + .setAction(async (args, hre) => { + if (!isFullyQualifiedName(args.contract)) { + throw new Error('Invalid fully qualified name of the contract.') + } + + const { contractName, sourceName: contractSource } = parseFullyQualifiedName(args.contract) + + if (!fs.existsSync(contractSource)) { + throw new Error(`Contract source ${contractSource} not found.`) + } + + await hre.run(TASK_COMPILE) + await submitSourcesToSourcify(hre, { + source: contractSource, + name: contractName, + address: args.address, + fqn: args.contract, + }) + }) diff --git a/yarn.lock b/yarn.lock index 7ad53b5d4..592a8a491 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5402,6 +5402,15 @@ form-data@^3.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + form-data@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"