pseudoyu

pseudoyu

Blockchain | Programming | Photography | Boyi
github
twitter
telegram
mastodon
bilibili
jike

Solidity Smart Contract Development - Using the Hardhat Framework

Introduction#

After studying the basics of smart contracts, Web3.py, and ethers.js in the previous articles, we have grasped the fundamental knowledge of directly interacting with blockchain networks through programming. Those who are unfamiliar can review:

However, in real complex business scenarios, we often use some further encapsulated frameworks, such as HardHat, Brownie, Truffle, etc. HardHat is the most widely used and has the most powerful plugin extensions. This series will start focusing on the use and best practices of the Hardhat framework from this article, and this article will complete its installation, configuration, and usage through a simple example.

This article is a summary of the tutorial by Patrick Collins titled 'Learn Blockchain, Solidity, and Full Stack Web3 Development with JavaScript', and it is highly recommended to watch the original tutorial video for more details.

You can click here to access the demo code repository.

Introduction to Hardhat#

hardhat_homepage

Hardhat is a JavaScript-based smart contract development environment that can be used to flexibly compile, deploy, test, and debug EVM-based smart contracts. It provides a series of toolchains to integrate code with external tools and offers a rich plugin ecosystem to enhance development efficiency. In addition, it also provides a local Hardhat network node that simulates Ethereum, offering powerful local debugging capabilities.

Its GitHub address is NomicFoundation/hardhat, and you can visit its official documentation for more information.

Using Hardhat#

Initializing a Project#

To build a Hardhat project from scratch, we need to have node.js and yarn environments installed in advance. This part can be followed according to the official instructions based on your system environment.

First, we need to initialize the project and install the hardhat dependency.

yarn init
yarn add --dev hardhat

yarn_add

Initializing Hardhat#

Next, we need to run yarn hardhat to initialize through an interactive command, configuring according to project needs. Our test demo will choose the default values.

hardhat_project_init

Optimizing Code Formatting#

VS Code Configuration#

I develop code locally using VS Code. You can install the Solidity + Hardhat and Prettier plugins for code formatting. You can open VS Code settings and add the following formatting configuration in settings.json:

{
    //...

    "[solidity]": {
        "editor.defaultFormatter": "NomicFoundation.hardhat-solidity"
    },
    "[javascript]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode",
    }
}

Project Configuration#

To unify the code formatting style among developers using the project, we can also configure prettier and prettier-plugin-solidity plugin support for the project:

yarn add --dev prettier prettier-plugin-solidity

yarn_add_prettier_plugin

After adding the dependencies, you can create .prettierrc and .prettierignore configuration files in the project directory for unified formatting:

My .prettierrc configuration is:

{
    "tabWidth": 4,
    "useTabs": false,
    "semi": false,
    "singleQuote": false
}

My .prettierignore configuration is:

node_modules
package.json
img
artifacts
cache
coverage
.env
.*
README.md
coverage.json

Compiling Contracts#

Unlike ethers.js, which requires a custom compile command, HardHat has a preset compile command. You can place contracts in the contracts directory and compile them using the yarn hardhat compile command:

hardhat_compile_contract

Adding dotenv Support#

Before we start writing deployment scripts, we first configure the dotenv plugin so that we can use dotenv to access environment variables. During development, we will deal with many sensitive information, such as private keys, and we would like to store them in a .env file or set them directly in the terminal, such as our RINKEBY_PRIVATE_TOKEN. This way, we can use process.env.RINKEBY_PRIVATE_TOKEN in the deployment script to get the value without explicitly writing it in the code, reducing the risk of privacy leakage.

Installing dotenv#

yarn add --dev dotenv

yarn_add_dotenv

Setting Environment Variables#

In the .env file, we can set environment variables, for example:

RINKEBY_RPC_URL=url
RINKEBY_PRIVATE_KEY=0xkey
ETHERSCAN_API_KEY=key
COINMARKETCAP_API_KEY=key

We can then read the environment variables in hardhat.config.js:

require("dotenv").config()

const RINKEBY_RPC_URL =
    process.env.RINKEBY_RPC_URL || "https://eth-rinkeby/example"
const RINKEBY_PRIVATE_KEY = process.env.RINKEBY_PRIVATE_KEY || "0xkey"
const ETHERSCAN_API_KEY = process.env.ETHERSCAN_API_KEY || "key"
const COINMARKETCAP_API_KEY = process.env.COINMARKETCAP_API_KEY || "key"

Configuring Network Environment#

Often, our contracts need to run on different blockchain networks, such as local testing, development, production environments, etc. Hardhat also provides a convenient way to configure network environments.

Starting the Network#

We can directly run a script to start a Hardhat built-in network, but this network only lives during the script execution. To start a local persistent network, we need to run the yarn hardhat node command:

hardhat_localhost_node

After execution, a test network and test accounts will be generated for subsequent development and debugging.

We can also generate our own test network nodes through platforms like Alchemy or Infura, recording their RPC_URL for program connection use.

Defining the Network#

After preparing the network environment, we can define the network in the project configuration hardhat.config.js:

const RINKEBY_RPC_URL =
    process.env.RINKEBY_RPC_URL || "https://eth-rinkeby/example"
const RINKEBY_PRIVATE_KEY = process.env.RINKEBY_PRIVATE_KEY || "0xkey"

module.exports = {
    defaultNetwork: "hardhat",
    networks: {
        locakhost: {
            url: "http://localhost:8545",
            chainId: 31337,
        },
        rinkeby: {
            url: RINKEBY_RPC_URL,
            accounts: [RINKEBY_PRIVATE_KEY],
            chainId: 4,

        },
    },
    // ...,
}

Scripts#

In a Hardhat project, we can implement deployment and other functions by writing scripts in the scripts directory and executing scripts through convenient commands.

Writing Deployment Scripts#

Next, we will start writing the deploy.js script.

First, we need to import the necessary packages from hardhat:

const { ethers, run, network } = require("hardhat")

Then we write the main method, which contains our core deployment logic:

async function main() {
    const SimpleStorageFactory = await ethers.getContractFactory(
        "SimpleStorage"
    )
    console.log("Deploying SimpleStorage Contract...")
    const simpleStorage = await SimpleStorageFactory.deploy()
    await simpleStorage.deployed()
    console.log("SimpleStorage Contract deployed at:", simpleStorage.address)

    // Get the current value
    const currentValue = await simpleStorage.retrieve()
    console.log("Current value:", currentValue)

    // Set the value
    const transactionResponse = await simpleStorage.store(7)
    await transactionResponse.wait(1)

    // Get the updated value
    const updatedValue = await await simpleStorage.retrieve()
    console.log("Updated value:", updatedValue)
}

Finally, we run our main method:

main()
    .then(() => process.exit(0))
    .catch((error) => {
        console.error(error)
        process.exit(1)
    })

Running the Script#

After completing the script writing, we can run the script using the run command provided by Hardhat.

If no network parameter is added, it defaults to using the hardhat network, and we can specify the network using the --network parameter:

yarn hardhat run scripts/deploy.js --network rinkeby

hardhat_deploy_rinkeby

Adding Etherscan Contract Verification Support#

After deploying the contract to the Rinkeby test network, you can view the contract address on Etherscan and verify it. We can operate through the website, but Hardhat provides plugin support for easier verification.

Installing the hardhat-etherscan Plugin#

We install the plugin using the command yarn add --dev @nomiclabs/hardhat-etherscan.

yarn_add_etherscan_verify_support

Enabling Etherscan Contract Verification Support#

After installation, we need to configure it in hardhat.config.js:

require("@nomiclabs/hardhat-etherscan")

module.exports = {
    // ...,
    etherscan: {
        apiKey: ETHERSCAN_API_KEY,
    },
    // ...,
}

Defining the Verify Method#

Next, we need to add the verify method in the deployment script deploy.js.

const { ethers, run, network } = require("hardhat")

async function verify(contractAddress, args) {
    console.log("Verifying SimpleStorage Contract...")
    try {
        await run("verify:verify", {
            address: contractAddress,
            constructorArguements: args,
        })
    } catch (e) {
        if (e.message.toLowerCase().includes("already verified!")) {
            console.log("Already Verified!")
        } else {
            console.log(e)
        }
    }
}

This method calls the run method from the hardhat package and passes a verify command along with a parameter { address: contractAddress, constructorArguements: args }. Since our contract may have already been verified on Etherscan, we perform a try...catch... error handling. If it has been verified, an error will be thrown, and a prompt message will be output without affecting our deployment process.

Setting Up Post-Deployment Calls#

After defining our verify method, we can call it in the deployment script:

async function main() {
    //...

    if (network.config.chainId === 4 && process.env.ETHERSCAN_API_KEY) {
        await simpleStorage.deployTransaction.wait(6)
        await verify(simpleStorage.address, [])
    }

    // ...
}

Here we have two special treatments.

First, we only need to verify the contract on the rinkeby network and not on local or other network environments. Therefore, we check network.config.chainId, and if it is 4, we execute the verification operation; otherwise, we do not execute it. Additionally, we only perform the verification operation when the ETHERSCAN_API_KEY environment variable is present.

Moreover, Etherscan may take some time to retrieve the contract address after deployment, so we configure .wait(6) to wait for 6 blocks before verifying.

The execution effect is as follows:

hardhat_verify_contract_etherscan

verified_contract_on_etherscan

After verifying through Etherscan, we can directly view the contract source code and interact with it.

interact_with_contract_on_etherscan

Contract Testing#

For smart contracts, most operations require deployment on-chain, interacting with assets, consuming gas, and any security vulnerabilities can lead to serious consequences. Therefore, we need to conduct detailed testing of smart contracts.

Hardhat provides complete testing and debugging tools, allowing us to write test scripts in the tests directory and run tests using the yarn hardhat test command.

Writing Test Scripts#

We will write a test-deploy.js test program for our deployment script, first importing the necessary packages:

const { assert } = require("chai")
const { ethers } = require("hardhat")

Then we write the test logic:

describe("SimpleStorage", () => {
    let simpleStorageFactory, simpleStorage
    beforeEach(async () => {
        simpleStorageFactory = await ethers.getContractFactory("SimpleStorage")
        simpleStorage = await simpleStorageFactory.deploy()
    })

    it("Should start with a favorite number of 0", async () => {
        const currentValue = await simpleStorage.retrieve()
        const expectedValue = "0"

        assert.equal(currentValue.toString(), expectedValue)
        // expect(currentValue.toString()).to.equal(expectedValue)
    })

    it("Should update when we call store", async () => {
        const expectedValue = "7"
        const transactionRespense = await simpleStorage.store(expectedValue)
        await transactionRespense.wait(1)

        const currentValue = await simpleStorage.retrieve()

        assert.equal(currentValue.toString(), expectedValue)
        // expect(currentValue.toString()).to.equal(expectedValue)
    })

In Hardhat's test scripts, we use describe to wrap the test class and it to wrap the test methods. We need to ensure that the contract is deployed before testing, so we call simpleStorageFactory.deploy() in the beforeEach method, and assign the returned simpleStorage object to the simpleStorage variable.

We use assert.equal(currentValue.toString(), expectedValue) to compare the execution result with the expected result, and we can use expect(currentValue.toString()).to.equal(expectedValue) as an alternative, which has the same effect.

Additionally, we can use it.only() to specify that only one of the test methods should be executed.

Executing Test Scripts#

We run the tests using yarn hardhat test, and we can specify the test method using yarn hardhat test --grep store.

hardhat_run_tests

Adding gas-reporter Support#

As mentioned above, gas is a resource we need to pay special attention to during development, especially on the Ethereum mainnet where it is particularly expensive. Therefore, we need to check gas consumption during testing. HardHat also has a gas-reporter plugin that can conveniently output gas consumption information.

Installing the gas-reporter Plugin#

We install the plugin using the command yarn add --dev hardhat-gas-reporter:

yarn_add_gas_reporter

Enabling gas-reporter Support#

We enable the plugin by adding gasReporter: true and additional configuration items in hardhat.config.js:

require("hardhat-gas-reporter")

const COINMARKETCAP_API_KEY = process.env.COINMARKETCAP_API_KEY || "key"

module.exports = {
    // ...,
    gasReporter: {
        enabled: true,
        outputFile: "gas-reporter.txt",
        noColors: true,
        currency: "USD",
        coinmarketcap: COINMARKETCAP_API_KEY,
        token: "MATIC",
    },
}

We can specify the output file, whether to enable colors, specify the currency, specify the token name, and specify the CoinMarketCap API key for further control over the output.

With the above configuration, running yarn hardhat test will produce the following output:

hardhat_add_gas_reporter_support_and_export

Adding solidity-coverage Support#

Contract testing is crucial for ensuring the correctness of business logic and security prevention, so we need to perform coverage testing on contracts. HardHat also has a solidity-coverage plugin that can conveniently output coverage information.

Installing the solidity-coverage Plugin#

We install the plugin using the command yarn add --dev solidity-coverage:

yarn_add_coverage_support

Enabling solidity-coverage Support#

We only need to import the package in hardhat.config.js to add coverage testing support:

require("solidity-coverage")

Running Coverage Tests#

You can run coverage tests using yarn hardhat coverage:

hardhat_coverage

Task#

In the above text, we have covered some basic functions and scripts of the hardhat library. In addition, we can also define some custom tasks for development and debugging purposes.

Writing a Task#

In Hardhat, we define tasks in the tasks directory. We will write a block-number.js task to get the block height:

const { task } = require("hardhat/config")

task("block-number", "Prints the current block number").setAction(
    async (taskArgs, hre) => {
        const blockNumber = await hre.ethers.provider.getBlockNumber()
        console.log(`Current Block Number: ${blockNumber}`)
    }
)

Tasks are created using the task() method and the execution function is set using the setAction() method. Here, taskArgs is an object containing all parameters, and hre is a HardhatRuntimeEnvironment object that can be used to access other resources.

Running the Task#

After defining it, our newly defined block-number task will appear in the AVAILABLE TASKS of the project command, and we can run the task using the command yarn hardhat block-number. Similarly, we can specify a specific network to run:

yarn hardhat block-number --network rinkeby

hardhat_run_tasks

Hardhat Console#

Finally, in addition to interacting with the chain/contracts through code, we can also use the Hardhat Console to debug the project and check the chain status, contract inputs, outputs, etc. We can open the Hardhat Console and interact by running the command yarn hardhat console.

hardhat_console

Summary#

This concludes my basic configuration and usage of the Hardhat framework. It is a powerful development framework, and I will continue to explore its more features and usage techniques in the future. If you are interested, please continue to follow, and I hope this will be helpful to everyone.

References#

  1. Learn Blockchain, Solidity, and Full Stack Web3 Development with JavaScript
  2. NomicFoundation/hardhat
  3. Hardhat Official Documentation
  4. Solidity Smart Contract Development - Basics
  5. Solidity Smart Contract Development - Mastering Web3.py
  6. Solidity Smart Contract Development - Mastering ethers.js
Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.