Verify your Smart Contracts on Etherscan Programmatically using Hardhat

Verify your Smart Contracts on Etherscan Programmatically using Hardhat

A step-by-step guide to verifying your smart contracts on Etherscan using Hardhat

Verifying your smart contracts is a crucial step in ensuring the transparency and trustworthiness of blockchain-based applications. Making the contract’s code publicly accessible and providing proof of deployment on the blockchain allows for independent verification of the contract’s functionality. This not only builds trust among users but also increases the contract’s visibility and credibility.

In addition to that, having a verified contract can make it more easily discoverable by potential users and can help to increase the contract’s visibility and credibility.

In this article, we will explore the process of programmatically verifying smart contracts on Etherscan using the Hardhat. By the end of this guide, you will have a thorough understanding of how to use Hardhat to automate the contract verification process on Etherscan, thus streamlining the smart contract deployment process.

Let’s delve into the details and learn how to verify smart contracts on Etherscan using Hardhat.

The process of programmatically verifying smart contracts on Etherscan can be accomplished using the Hardhat in two different ways.

  1. Writing a custom verification script using Hardhat, which can be run at any time to verify contracts on Etherscan.

  2. Implementing a professional approach to automatically verify smart contracts upon deployment to the testnet or mainnet.

As previously discussed in Learn to Deploy Smart Contracts more Professionally with Hardhat article, managing smart contract scripts can present challenges for developers as a project grows in size and complexity. As the project scales, the complexity of managing smart contract scripts also increases, making it a headache for developers to keep track of the changes and maintain the integrity of the project.

In order to fully understand and implement the second approach for automating the verification process, it is recommended that you first read the above article.

Projects Configuration for Verification

  1. Create a new Hardhat Project

  2. Remove the default contract, script and test files provided by Hardhat. These files can be found in the contracts/, scripts/, and test/ folders, they should be removed to avoid confusion and potential errors.

  3. Get the API Key from Etherscan https://etherscan.io/.

  4. Create the .env file at the root location.

  5. Add the .env file inside the .gitignore file. Make sure you must follow this step because later we will add the PRIVATE_KEY in this file.

  6. Create an Etherscan API key variable inside .env the file and add your key as a value for this variable like this ETHERSCAN_API_KEY=DR9... .

  7. Install the dotenv a package so we can load the env file variables in our scripts. Run the command in the terminal npm i --save-dev dotenv --force .

  8. Import the dotenv package inside the hardhat.config.js or hardhat.config.ts file like this require(“dotenv”).config() (JavaScript) or import “dotenv/config” (TypeScript).

Add the Sample Smart Contract for Verification

This smart contract will be utilized for verification purposes.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract SimpleStorage {
    uint private storedData;

    function set(uint x) public {
        storedData = x;
    }

    function get() public view returns (uint) {
        return storedData;
    }
}
  1. Create a file called SimpleStorage.sol inside the contracts folder and add this to it.

  2. Compile the smart contract by running the command npx hardhat compile

Writing a Custom Verification Script

To verify the functionality of this smart contract, it must first be deployed on a specific network. Utilizing the deployed contract’s address, the smart contract can then be verified on Etherscan.

To do this Create a file called verify-simple-storage.js or verify-simple-storage.js inside the scripts folder and add this code inside it;

Note: Please note that both JavaScript and TypeScript code will be provided throughout this article. Kindly select the appropriate code examples based on the language being utilized in your implementation.

// JavaScript
import { ethers, network, run } from "hardhat";

async function verifyContract() {
    const chainId = network.config.chainId;

    const simepleStorageFactory = await ethers.getContractFactory(
        "SimpleStorage"
    );

    const args = [];

    console.log(`Deploying...`);
    const simpleStorage = await simepleStorageFactory.deploy(args);
    await simpleStorage.deployed();
    console.log(`Deployed!`);
    console.log(`Simple Storage Address: ${simpleStorage.address}`);

    console.log(`Waiting for blocks confirmations...`);
    await simpleStorage.deployTransaction.wait(6);
    console.log(`Confirmed!`);

    // * only verify on testnets or mainnets.
    if (chainId != 31337 && process.env.ETHERSCAN_API_KEY) {
        await verify(simpleStorage.address, args);
    }
}

const verify = async (contractAddress, args) => {
    console.log("Verifying contract...");
    try {
        await run("verify:verify", {
            address: contractAddress,
            constructorArguments: args,
        });
    } catch (e) {
        if (e.message.toLowerCase().includes("already verified")) {
            console.log("Already verified!");
        } else {
            console.log(e);
        }
    }
};

verifyContract()
    .then(() => process.exit(0))
    .catch((error) => {
        console.log(error);
        process.exit(1);
    });
// TypeScript
import { ethers, network, run } from "hardhat";

async function verifyContract(): Promise<void> {
    const chainId = network.config.chainId!;

    const simepleStorageFactory = await ethers.getContractFactory(
        "SimpleStorage"
    );

    const args: any[] = [];

    console.log(`Deploying...`);
    const simpleStorage = await simepleStorageFactory.deploy(args);
    await simpleStorage.deployed();
    console.log(`Deployed!`);
    console.log(`Simple Storage Address: ${simpleStorage.address}`);

    console.log(`Waiting for blocks confirmations...`);
    await simpleStorage.deployTransaction.wait(6);
    console.log(`Confirmed!`);

    // * only verify on testnets or mainnets.
    if (chainId != 31337 && process.env.ETHERSCAN_API_KEY) {
        await verify(simpleStorage.address, args);
    }
}

const verify = async (contractAddress: string, args: any[]) => {
    console.log("Verifying contract...");
    try {
        await run("verify:verify", {
            address: contractAddress,
            constructorArguments: args,
        });
    } catch (e: any) {
        if (e.message.toLowerCase().includes("already verified")) {
            console.log("Already verified!");
        } else {
            console.log(e);
        }
    }
};

verifyContract()
    .then(() => process.exit(0))
    .catch((error) => {
        console.log(error);
        process.exit(1);
    });

Let me explain what’s going on; (I am going to explain JavaScript but the Concepts are the same for both).

async function verifyContract() {}

As simple we are creating a verifyContract JavaScript function.

verifyContract()
  .then(() => process.exit(0))
  .catch((error) => {
      console.log(error);
      process.exit(1);
  });

Calling it inside the script so the code gets executed when we run the script.

const chainId = network.config.chainId;

Within the function, we utilize the network object provided by Hardhat to obtain the chainId of the network. This obtained chainId is then utilized to verify the smart contract exclusively on either testnets or mainnets.

const simepleStorageFactory = await ethers.getContractFactory(
        "SimpleStorage"
    );

By utilizing the getContractFactory function from the ethers.js library, we can instantiate a factory object for our smart contract.

The getContractFactory function in the Ethereum JavaScript library ethers.js is used to create a contract factory object for a specific contract. This factory object can then be used to deploy new instances of the contract to the Ethereum blockchain. The factory takes the ABI (Application Binary Interface) and bytecode of the contract as input. The ABI is a JSON representation of the contract's interface, which describes the functions and events that the contract exposes. The bytecode is the compiled code of the contract that gets deployed to the blockchain. Once the factory is created, it can be used to deploy new instances of the contract by calling the deploy method and passing in any necessary constructor arguments.

const args = [];

console.log(`Deploying...`);
const simpleStorage = await simepleStorageFactory.deploy(args);
await simpleStorage.deployed();
console.log(`Deployed!`);
console.log(`Simple Storage Address: ${simpleStorage.address}`);

The variable args represents the list of arguments for the smart contract constructor. However, in this instance, it is left empty as the SimpleStorage contract does not require any arguments to be passed during instantiation.

We use the deploy function of the contractFactory to deploy the smart contract, passing in any necessary arguments as specified. This results in the instantiation of the simpleStorage object, representing the deployed smart contract.

console.log(`Waiting for blocks confirmations...`);
await simpleStorage.deployTransaction.wait(6);
console.log(`Confirmed!`);

We use the wait function of the deployTransaction of the smart contract instance to wait for a confirmation of 6 blocks. Waiting for block confirmations before verification of the smart contract ensures that the contract is properly recorded on the blockchain and its bytes codes are available for verification.

// * only verify on testnets or mainnets.
if (chainId != 31337 && process.env.ETHERSCAN_API_KEY) {
    await verify(simpleStorage.address, args);
}

This section of the code establishes that the verify function should only be called under certain conditions: the network must not be hardhat or localhost and an Etherscan API key must be present in the environment file. Once these conditions have been met, the verify function is invoked, passing in the appropriate contract address and arguments.

Why can we not Verify for the Hardhat network?

As Hardhat is a local network, running exclusively on the user’s machine, it does not have a corresponding public blockchain for Etherscan to utilize for the verification of smart contracts.

    solidity: "0.8.17",
    networks: {
        hardhat: {
            chainId: 31337,
        },
        localhost: {
            chainId: 31337,
        },
    },
    etherscan: {
        apiKey: process.env.ETHERSCAN_API_KEY,
    },

At this step, It is important to ensure that the networks object within the hardhat configuration file contains Hardhat and Localhost networks with a chainId of 31337, and that an etherscan object is also present.

If you are using the most recent version of Hardhat that includes the @nomicfoundation/hardhat-toolbox plugin then the verify task is included by default. However, if this is not the case, it is necessary to install the hardhat-etherscan plugin and configure it within the Hardhat configuration file in order for the verification process to be performed. Read more about it here Migration away from hardhat-waffle.

await run("verify:verify", {
    address: contractAddress,
    constructorArguments: args,
});

We utilize the run function provided by Hardhat to execute the verify task, passing in the appropriate contract address and smart contract constructor arguments. This step completes the process of verifying the smart contract on the Etherscan.

As seen in the code, we are calling the verify:verify subtask, which is a part of the larger verify task. This subtask specifically handles the verification of the smart contract on the Etherscan. It is important to note that the verify task contains various other subtasks that are not called in this particular instance.

try {
        await run("verify:verify", {
            address: contractAddress,
            constructorArguments: args,
        });
    } catch (e) {
        if (e.message.toLowerCase().includes("already verified")) {
            console.log("Already verified!");
        } else {
            console.log(e);
        }
    }

We have wrapped the execution of the run function within a try-catch block to handle any errors that may occur. In the event that the smart contract has already been verified, an error will be thrown and using the error message, we are informing the user that the contract has already been verified.

// JavaScript
npx hardhat run scripts/verify-simple-storage.js --network goerli

// TypeScript
npx hardhat run scripts/verify-simple-storage.ts --network goerli

By executing the above command, we will be able to deploy the smart contract and subsequently verify it on Etherscan after waiting for 6 block confirmations. This allows for easy verification of the smart contract on the Ethereum blockchain.

// JavaScript
goerli: {
    chainId: 5,
    url: process.env.GOERLI_RPC_URL,
    accounts: [process.env.PRIVATE_KEY],
    saveDeployments: true,
},

// TypeScript
goerli: {
    chainId: 5,
    url: process.env.GOERLI_RPC_URL,
    accounts: [process.env.PRIVATE_KEY!],
    saveDeployments: true,
},

It is important to note that before running the above command, the Goerli network should be added to the networks object in the configuration file. Additionally, the GOERLI_RPC_URL and PRIVATE_KEY environment variables should be defined in the env file to ensure proper deployment and verification on the Goerli network.

The GOERLI_RPC_URL, which is required for connecting to the Goerli network, can be obtained from a platform such as Alchemy. The PRIVATE_KEY, used for signing transactions, can be retrieved from a wallet service like MetaMask.

Note: It is important to keep in mind that the private key should be kept secure, and never shared with anyone. Additionally, it is important to ensure that the env file containing the private key is added to the .gitignore file before committing any code to a version control system, to prevent the private key from being accidentally exposed to the public.

Automated Verification of Smart Contracts

Using the above script approach deploying and verifying smart contracts can be a straightforward process when working with a small number of contracts. However, as the number of contracts increases, this manual approach can become unwieldy and time-consuming for developers. The repetitive nature of running deployment and verification commands for each individual contract can pose a significant challenge for large-scale projects. A more efficient and scalable approach is needed to effectively manage and deploy a large number of smart contracts.

It is important to note that in order to fully grasp this approach, a basic understanding of deploying smart contracts using the hardhat-deploy plugin is required. If you are not familiar with this method, it is recommended that you first read Learn to Deploy Smart Contracts more Professionally with Hardhat article before proceeding.

A best practice is to create a separate file for the verification function. This allows the function to be easily imported and reused across multiple files, making the deployment and verification process more efficient and streamlined.

Create a utils folder at the root location of the project. Within this folder, create a file named verify.js or verify.ts . Add the above verify function inside this file and export it.

// JavaScript
const { verify } = require("../utils/verify");

// TypeScript
import verify from "../utils/verify";

Import the verify function in your deploy file.

// JavaScript
    const args = [];
    const simpleStorage = await deploy("SimpleStorage", {
        from: deployer,
        log: true,
        args: args,
        waitConfirmations: chainId == 31337 ? 1 : 6,
    });

    // * only verify on testnets or mainnets.
    if (chainId != 31337 && process.env.ETHERSCAN_API_KEY) {
        await verify(simpleStorage.address, args);
    }
 // TypeScript
    const args: any[] = [];
    const simpleStorage: DeployResult = await deploy("SimpleStorage", {
        from: deployer,
        log: true,
        args: args,
        waitConfirmations: chainId == 31337 ? 1 : 6,
    });

    // * only verify on testnets or mainnets.
    if (chainId != 31337 && process.env.ETHERSCAN_API_KEY) {
        await verify(simpleStorage.address, args);
    }

Execute the verification function, providing the deployed contract’s address and any constructor arguments utilized during the deployment process.

npx hardhat deploy --network goerli

With this approach, a single command can be used to deploy and verify all of the smart contracts located within the deploy folder. This streamlined process eliminates the need for manually deploying and verifying each individual contract, resulting in a more efficient and manageable development process.

Conclusion

In conclusion, verifying smart contracts on the Ethereum blockchain is an essential aspect of building trust and transparency in blockchain-based applications. By making the contract’s code publicly accessible and providing proof of deployment on the blockchain, developers can increase the visibility, credibility, and discoverability of their contracts. Verifying smart contracts is not just a good practice for blockchain-based application development, but it is also a way to increase the user’s trust and adoption of the application.

This comprehensive guide aims to provide assistance to developers of all skill levels and is intended to be beneficial for a wide range of developers. Despite its length, the information provided is intended to be easily understandable and actionable for all readers.

If you found this content helpful, please consider giving it a clap and leaving any feedback for future improvements. Your suggestions and comments are greatly appreciated and will help make these articles even more valuable for you and other readers.

Be sure to follow me to receive updates on my future articles and stay informed of new content.

Thank you

Ali Murtaza Memon