OpenZeppelin - A Hands-On Learning Experience

Posted by Panos Matsinopoulos on 15/Jun/2024 (11:24)
Hero Image by DESPOINA MATSINOPOULOU from Pixabay

OpenZeppelin - A Hands-On Learning Experience

Today I decided to have a new learning experience.

It's called OpenZeppelin, which is a set of libraries and tools to code, deploy and operate Smart Contracts.

OpenZeppelin - Learn

There is a page called Learn which takes you through the following activities:

  • Setting up a Node Project
  • Developing Smart Contracts
  • Deploying and Interacting
  • Writing Automated Tests
  • Connecting to Public Test Networks
  • Upgrading Smart Contracts
  • Preparing for Mainnet

I would like to give you some hints and present you with my notes and findings for each one of these sections.

Here we are:

Show me the code: This is the link to the source of my work.

Setting up a Node Project

Version Management with asdf

I use asdf to manage the versions of the tools I use in a project.

For this particular learning experience I used:

  • Node.js 18.17.0
  • Yarn 1.22.19
  • direnv 2.34.0

About the direnv, I am going to tell you, in a while, how I use it.

Truffle vs Hardhat

The tutorial is suggesting that I use one of Truffle or Hardhat.

According to Truffle website, the Truffle Suite is being sunset. I can confirm that from the relevant Consensys blog post. Consensys is announcing partnership with Hardhat.

Hence, I went with Hardhat. See next section about which version.

Developing Smart Contracts

Hardhat

Which version of hardhat to install? At the time of writing this one, the latest version was 2.22.5. This is the version I went with. That was not a problem.

Watch out, that the suggested npx hardhat command is deprecated and it is suggested that you use:

$ npx hardhat init

to initialize your project.

This presents a list of options to choose from. I chose

Create an empty hardhat.config.js.

OpenZeppelin Contracts

This is where the good stuff starts. I am introduced to one of OpenZeppelin Contracts, Ownable.

I am being asked to install @openzeppelin/contracts. The latest version at the time of writing was 5.0.2. I used that one, with no problems.

Deploying and Interacting

Local Blockchain

Thanks to Hardhat Network I am able to use a local Ethereum-like testnet for development purposes.

This is excellent!

Deploying a Contract

The script that is used to deploy the contract, scripts/deploy.js requires ethers and hardhat-ethers.

Problem: Versions

The latest versions of these packages are:

  • Ethers: 6.13.0
  • Hardhat Ethers: 3.0.6. In fact the latest version of this one is coming in the package @nomicfoundation/hardhat-ethers

These versions do not work with the script/deploy.js as defined by the OpenZeppelin tutorial.

I used these:

  • Ethers: 5.7.2 with yarn add ethers@5.7.2 and
  • Hardhat Ethers: 2.2.3 with yarn add @nomiclabs/hardhat-ethers@2.2.3.

See how the version 2.x.y of hardhat-ethers is in different namespace: @nomiclabs and not @nomicfoundation.

Hardhat Config for Deployment

Which takes us to the script hardhat.config.js which needs to require the correct hardhat-ethers package:

require("@nomiclabs/hardhat-ethers");
...

instead of

require("@nomicfoundation/hardhat-ethers");

Interacting

That went smoothly. Nothing to note here.

Writing Automated Tests

This is one of my favourite parts in software engineering. Automated tests.

Chai

Which version?

The latest, at the time of writing, was 5.5.1. But I had to go back to 4.4.1 to work well with my current setup. It seems that 5.5.1 doesn't play well with module loading with the current set up of the hardhat project.

So, do:

yarn add --dev chai@4.4.1

instead of yarn add --dev chai.

Location of Test Script

The tutorial is suggesting test/Box.test.js. I went with test/contracts/Box.test.js to mirror the location of the contract source file.

OpenZeppelin Test Helpers vs Hardhat Chai Matchers

As the tutorial is suggesting, I went with Hardhat Chai Matchers rather than OpenZeppelin Test Helpers, because the latter is web3.js based. The former is ethers based.

So, yes to Hardhat Chai Matchers, not to OpenZeppelin Test Helpers for this project. I am also guessing that OpenZeppelin will soon have to adapt their tutorial to suggest using Hardhat only since Truffle (which uses web3.js) is becoming obsolete.

Hardhat Chai Matchers Version

But again, which version of Hardhat Chai Matchers?

At the time of this writing, the latest version was 2.0.7 published in the @nomicfoundation namespace.

But, this doesn't work well with the chai and the ethers version that we have gone so far.

So, I had to install 1.0.6.

Hence:

yarn add --dev @nomicfoundation/hardhat-chai-matchers@1.0.6

instead of yarn add --dev @nomicfoundation/hardhat-chai-matchers.

Correct test/contracts/Box.test.js

So, the matching version of the test/contracts/Box.test.js is the following:

Note that I have also reorganized the outline of the tests. I like to nest and use describe and context more than it is suggested in the original Box.test.js script by the tutorial. My approach makes the tests easier to read and deliver their testing purpose to the reader.

Connecting to Public Test Networks

It's amazing to be able to see your contracts deployed to the public, even if it is a public test network and not the real Ethereum Mainnet.

I went with Sepolia.

But, first one needs to use a service via which they will be able to connect to an Ethereum network.

I went with Alchemy. Alchemy is a suite of tools to build web3 apps and deploy them to the network, the network being any test or real network.

Important Create an Alchemy account and a new app inside it. For the app you create, you are going to get an API Key.

Alchemy API Key
Alchemy API Key

Adding Sepolia to Hardhat Networks

I then added Sepolia to the list of Hardhat networks supported:

File: hardhat.config.js

require("@nomiclabs/hardhat-ethers");

// Ensure your configuration variables are set before executing the script
const { vars } = require("hardhat/config");

const ALCHEMY_API_KEY = vars.get("ALCHEMY_API_KEY");

// This is the private key of my SEPOLIA Account I use for testing.
// I save the value inside the ".envrc" which is not checked-in.
// +direnv+ makes sure that the environment variable is set accordingly.
//
const SEPOLIA_TEST_NET_PRIVATE_KEY = vars.get("SEPOLIA_TEST_NET_PRIVATE_KEY");

/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
  solidity: "0.8.24",
  networks: {
    sepolia: {
      url: `https://eth-sepolia.g.alchemy.com/v2/${ALCHEMY_API_KEY}`,
      accounts: [SEPOLIA_TEST_NET_PRIVATE_KEY],
    },
  },
};

Look how the url value is matching the https endpoint suggested by the Alchemy API dialogue (screenshot above).

Hardhat Configuration

Hardhat is configured using configuration variables. It is all explained here: Configuration Variables.

I personally like to use environment variables. Hence, I am following the instructions outlined here: Overriding configuration variables with environment variables, in combination with direnv.

Hence, inside my .envrc file I have something like this:

export HARDHAT_VAR_ALCHEMY_API_KEY='...'

Hence, the hardhat.config.js script can read the value of it.

Important: do not commit the .envrc file to git. Add .envrc to you .gitignore file.

But what about the SEPOLIA_TEST_NET_PRIVATE_KEY? Here it is:

Signing Requests

In order for you to be able to commit transactions to the network you need to sign them and tell who you are. You need an Ethereum account. For testing purposes I am not using my real account. I am a little cautious. I have created another account to play with.

And since each account has a public and a private key, here, we have to declare the private key. Because this is needed to sign requests for transactions.

We have to declare it as accounts value in the sepolia Hardhat network configuration.

Now, Hardhat is ready to send requests to Sepolia, via Alchemy.

Note that the tutorial is suggesting another approach to specifying how the transaction requests will be signed. The tutorial uses the package mnemonics to create the Ethereum account. I prefer to create my accounts with MetaMask.

Adding Funds to Testnet Account

In order to execute transactions in an Ethereum network, your account needs to have some Ether in order to pay for the gas necessary for the transaction.

This is true even if you are about to carry out a transaction on a testnet, like Sepolia. The only difference is that the account balance that will carry out transactions on a testnet can have fake/test ETH and not real.

Maybe this is the most cumbersome step in the process? Maybe, but it does worth trying.

The testnets usually have a way to add test ETHs using a process called faucet. For example, this is the Sepolia Faucet.

But, as you can see from the message on their site

To prevent bots and abuse, this faucet requires a minimum mainnet balance of 0.001ETH on the wallet address being used.

This is what I did. I made sure that the Ethereum account that I use for testing and development (read earlier about the account with the private key in the environment variable SEPOLIA_TEST_NET_PRIVATE_KEY) it had some real ETH in the mainnet. With some real ETH, I managed to activate the Ethereum Sepolia Faucet to give me some test ETH.

Remember Ethereum account balances are per network. So the same account can have different balances in different networks. Here I am talking about network-native currencies, the currency needed to pay for gas and executing transactions. If we were to talk about tokens, then an account/wallet could potentially have multiple balances in the same network, one balance per token the account holds. But, here, I am not talking about tokens. I am talking about the native currency of the network.

With test funds in the testnet, I can do transactions. It works.

Upgrading Smart Contracts

I can't imagine anyone deploying production contracts without them having the ability to be upgraded.

So, this section was fascinating.

Which Library Version?

The tutorial is asking you to install @openzeppelin/hardhat-upgrades the latest version of which is 3.1.1, at the time of writing.

That was another compatibility problem for me.

So, I had to downgrade to 1.28.0.

Hence:

yarn add --dev @openzeppelin/hardhat-upgrades@1.28.0

Upgradeable Contracts Cannot Have a Constructor

That part of the tutorial required the library @openzeppelin/contracts-upgradeable, version 5.0.2, at the time of writing.

So, install that as a normal dependency, not a dev one.

yarn add @openzeppelin/contracts-upgradeable

Otherwise, I didn't have any other surprises in this section.

Preparing for Mainnet

Verifying Source Code

This was super interesting too. I should verify the source code of my contract after deployment to Mainnet.

I followed the instructions to verify using the Hardhat Etherscan plugin.

Just a note here. The tutorial is suggesting that we use the @nomiclabs/hardhat-etherscan library which is now deprecated. The deprecation warning is pointing to better using the @nomicfoundation/hardhat-verify instead. Anyway. Current tutorial experience worked with the deprecated package without problem.

Closing Note

These were my notes while I practised on OpenZeppelin Learn.

They may be helpful to you if you decide to practise too.

Thanks for reading. You can always reach out to me on:


If you liked this post, you can buy me a cup of coffee to keep me company when writing the next one.