How to deploy your first smart contract on Ethereum with Solidity and Hardhat

StErMi
12 min readSep 17, 2021

--

Hardhat

I was planning to release a tutorial on how to deploy your first NFT smart contract but while I was writing it I understood that more than half of the content was about how to set up Hardhat to have the perfect development environment ready to go for your next project.

So let’s start from here and when I’ll release the NFT tutorial you can leverage the work we have done today to be ready instantly to start developing your next smart contract. Shall we proceed?

What are you going to learn today?

  • Configure Hardhat to compile, deploy, test, and debug your Solidity code
  • Write a smart contract
  • Test your smart contract
  • Deploy your smart contract on Rinkeby
  • Get your smart contract verified by Etherscan

Hardhat

Hardhat is the core tool we will be using today and a must-have if you need to develop and debug your smart contract locally before deploying it on the test net and then on the main net.

What is Hardhat?

Hardhat is a development environment to compile, deploy, test, and debug your Ethereum software. It helps developers manage and automate the recurring tasks that are inherent to the process of building smart contracts and dApps, as well as easily introducing more functionality around this workflow. This means compiling, running, and testing smart contracts at the very core.

Why use hardhat?

  • You can run Solidity locally to deploy, run, test, and debug your contracts without dealing with live environments.
  • You get Solidity stack traces, console.log, and explicit error messages when transactions fail. Yeah, you heard it right, console.log in your Solidity contract!
  • Tons of plugins to add cool functionalities and allows you to integrate your existing tools. We will use a couple of them to feel that magic!
  • Full native support for TypeScript.

Useful links to learn more

Create your first Hardhat project

It’s time to open your Terminal, your Visual Studio code IDE, and start building!

What project are we going to build?

In this tutorial, we are going to build an “easy” smart contract. It will be pretty stupid but our main goal here is to configure hardhat and deploy the contract, not for the contract purpose.

What will the contract do?

  • Track the “purpose” of the contract in a string that will be publicly visible
  • You can change the purpose only if you pay more than the previous owner
  • You cannot change the purpose if you are already the owner
  • You can withdraw your ETH only if you are not the current purpose’s owner
  • Emit a PurposeChange event when the purpose has changed

Create the project, init it and configure Hardhat

Open your terminal and let’s go. We will call our project world-purpose!

mkdir world-purpose
cd world-purpose
npm init -y
yarn add --dev hardhat
npx hardhat

With npx hardhat the hardhat wizard will kickstart helping you configure your project.

  • Choose Create a basic sample project
  • Choose the default Hardhat project root because in this tutorial we are only configuring hardhat.
  • Confirm to add the .gitignore
  • Confirm to install the sample project's dependecies with yarn this will install some required dependencies for your project that we will use along the way

Now your project structure should have these files/folder

  • contracts where all your contracts will be stored
  • scripts where all your scripts/tasks will be stored
  • test where your smart contract tests will be stored
  • hardhat.config.js where you will configure your hardhat project

Important hardhat commands and concepts you should master

  • Hardhat Configuration file
  • Hardhat Tasks
  • npx hardhat node — Run the node task to start a JSON-RPC server on top of Hardhat Network (your local ethereum blockchain)
  • npx hardhat test — To run tests stored in the test folder
  • npx hardhat compile — To compile the entire project, building your smart contracts
  • npx hardhat clean — To clean the cache and delete compiled smart contracts
  • npx hardhat run —-network <network> script/path — To run a specific script in a specific network

Add TypeScript Support

As we said Hardhat supports typescript natively and we will be using it. Using javascript or typescript doesn’t change much but this is my personal preference because I think that it allows you to make fewer errors and understand better how to use external libraries.

Let’s install some needed dependencies

yarn add --dev ts-node typescript
yarn add --dev chai @types/node @types/mocha @types/chai

Change the hardhat configuration file extension to .ts and update the content with this

Now create a default tsconfig.json in the root of your project with this content

{
"compilerOptions": {
"target": "es2018",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"outDir": "dist"
},
"include": ["./scripts", "./test"],
"files": ["./hardhat.config.ts"]
}

Now update also the test file and script file to the typescript file type .ts and change their code.

Let’s test that everything is working correctly

npx hardhat test
npx hardhat node
npx hardhat run --network localhost scripts/sample-script.ts

Let’s add some Hardhat plugins and some code utilities

add solhint, solhint-plugin-prettier, and hardhat-solhint plugin

Now we want to add support to solhint a linting utility for Solidity code that will help us to follow strict rules while we develop our smart contract. These rules a useful for both follow the code style standard best practice and adhering to the best security approaches.

solhint+solhint prettier

yarn add --dev solhint solhint-plugin-prettier prettier prettier-plugin-solidity

Add a .solhint.json configuration file in your root folder

{
"plugins": ["prettier"],
"rules": {
"prettier/prettier": "error"
}
}

Add a .prettierrc config file and add styles as you prefer. This is my personal choice

{
"arrowParens": "always",
"singleQuote": true,
"bracketSpacing": false,
"trailingComma": "all",
"printWidth": 120,
"overrides": [
{
"files": "*.sol",
"options": {
"printWidth": 120,
"tabWidth": 4,
"useTabs": false,
"singleQuote": false,
"bracketSpacing": false,
"explicitTypes": "always"
}
}
]
}

If you want to know more about solhint and its prettier plugin checkout their documentation site

hardhat-solhint

If you want to know more follow the Hardhat solhint plugin documentation.

yarn add --dev @nomiclabs/hardhat-solhint

and update the hardhat config adding this line to the import

import “@nomiclabs/hardhat-solhint”;

typechain for typed contract support

This part is totally optional. As I said I love typescript and I like to use it everywhere whenever it’s possible. Adding support to contract types allow me to know perfectly which functions are available, which parameters are needed of which type, and what are they returning.

You could totally go without it but I highly suggest you follow this step.

Why typechain?

  • Zero Config Usage — Run the compile task as normal, and Typechain artifacts will automatically be generated in a root directory called typechain.
  • Incremental generation — Only recompiled files will have their types regenerated
  • Frictionless — return type of ethers.getContractFactory will be typed properly - no need for casts

If you want to dig deeper into these projects and know all the possible configuration options you can follow these links:

Let’s do it

yarn add --dev typechain @typechain/hardhat @typechain/ethers-v5

Add these imports to your Hardhat config file

import '@typechain/hardhat'
import '@nomiclabs/hardhat-ethers'
import '@nomiclabs/hardhat-waffle'

Add "resolveJsonModule": true to your tsconfig.json

Now when you compile your contract typechain will generate the corresponding types into the typechain folder and you will be able to use it in your tests and web app!

Updated the tests file to use Typechain generated types

Update scripts in package.json

This part is totally optional but it allow you to run command faster and preconfigure them. Open your package.json and override the scripts section with these commands:

"clean": "npx hardhat clean",
"chain": "npx hardhat node",
"deploy": "npx hardhat run --network localhost scripts/deploy.ts",
"deploy:rinkeby": "npx hardhat run --network rinkeby scripts/deploy.ts",
"test": "npx hardhat test"

Now if you want to simply run the Hardhat node you can write in your console yarn chain and boom! We are ready to go!

If you have skipped the TypeScript part and you want to use only Javascript just change those .ts back to .js and everything will work as expected.

Developing the smart contract

Ok, now we are really ready to go. Rename Greeter.sol in your contracts folder to WorldPurpose.sol and we will start building from here.

Our contract needs to do these things:

  • Track the “purpose” of the contract in a struct
  • You can change the purpose only if you pay more than the previous owner
  • You cannot change the purpose if you are already the owner
  • You can withdraw your ETH only if you are not the current purpose’s owner
  • Emit a PurposeChange event when the purpose has changed

Concepts you should master

Smart Contract Code

Let’s start by creating a Struct to store the Purpose information

/// @notice Purpose struct
struct Purpose {
address owner;
string purpose;
uint256 investment;
}

Let’s now define some state variables to track both the current Purpose and user’s investment

/// @notice Track user investments
mapping(address => uint256) private balances;
/// @notice Track the current world purpose
Purpose private purpose;

Last but not least define an Event that will be emitted when a new World Purpose has been set

/// @notice Event to track new Purpose
event PurposeChange(address indexed owner, string purpose, uint256 investment);

Let’s create some utility functions to get the current purpose and the user’s balance that is still in the contract. We need to do that because balances and purpose variables are private so they cannot be accessed directly from external contract/web3 apps. We need to expose them via these functions

/**
@notice Getter for the current purpose
@return currentPurpose The current active World purpose
*/
function getCurrentPurpose() public view returns (Purpose memory currentPurpose) {
return purpose;
}
/**
@notice Get the total amount of investment you have made. It returns both the locked and unloacked investment.
@return balance The balance you still have in the contract
*/
function getBalance() public view returns (uint256 balance) {
return balances[msg.sender];
}

Now let’s create the setPurpose function. It takes one parameter as input: _purpose . The function must be payable because we want to accept some Ether in order to set the purpose (those ethers will be withdrawable by the owner the purpose has been overridden by someone else).

The transaction will revert if some conditions are not met:

  • _purpose input parameter is empty
  • the msg.value (amount of ether sent with the transaction) is empty (0 ethers)
  • the msg.value is less than the current purpose
  • the current world purpose owner tries to override his purpose (msg.sender must be different from the current owner)

If everything works we set the new purpose, update the balance and emit the event.

Now we want to allow users to withdraw their investment from previous purposes. Please note that only the current purpose’s investment is “locked”. It will be unlocked only when a new person will set the new purpose.

Deploy it locally just to test that everything works as expected. You should see something like this:

Contract deployment on local blockchain successful

Add local Test

Now I will not go into detail about the code inside the testing file but I’ll explain the concept behind it. You should always create test cases for your contract. It’s a fast way to understand if the contact’s logic is working as expected and allows you to prevent deploying something that it’s not working.

My current workflow is like this: I take every function and I test that they do what I expect them to do in both success cases and revert cases. Nothing more, nothing less.

When you write tests for Solidity contracts you are going to use Waffle. Head over to their website to have an overview of the library, it’s really well made and offers a lot of utilities.

Now, delete the file you have in your test folder, create a new one called worldpurpose.ts and replace the content with this

Execute your tests with yarn test and see if everything pass as expected

Tests execution

Deploy your contract on test net

===== HUGE DISCLAIMER START =====

DO NOT USE YOUR MAIN WALLET PRIVATE FOR TESTING PURPOSES

USE A BURNER WALLET OR CREATE A NEW WALLET

===== HUGE DISCLAIMER END =====

We have already seen how to deploy the contract in our local hardhat chain but now we want to do it for the Rinkeby test net (the procedure is the same one for the main net but it’s not the scope of the tutorial).

To deploy on the local chain we just need to type on console this command

npx hardhat run --network localhost scripts/deploy.ts

To deploy your contracts on another network we just need to change the parameter value of --network but before doing that need to do some preparation step.

Get some ETH from testnet Faucet

Deploying a contact cost gas so we need some from a Faucet. Choose one test network and head over to the faucet to get funds.

In our case, we decided to use Rinkeby so go and get some funds over there.

Choose an Ethereum Provider

In order to deploy our contract, we need a way to interact directly with the Ethereum network. Nowadays we have two possible options:

In our case, we are going to use Alchemy, so go over there, create an account, create a new app and grab the Rinkeby API Key.

Update the Hardhat Config file

Install the dotenv dependencies, this nodejs library allow us to create a .env environment file to store our variables without exposing them to the source code.

yarn add --dev dotenv

Now create in the root of your project a .env file and paste all the information you have collected

RENKEBY_URL=https://eth-rinkeby.alchemyapi.io/v2/<YOUR_ALCHEMY_APP_ID>PRIVATE_KEY=<YOUR_BURNER_WALLET_PRIVATE_KEY>

I can’t stress enough. Don’t use your main wallet private case for testing purposes. Create a separate wallet or use a burning wallet to do this type of testing!

The last step is to update the hardhat.config.ts file to add dotenv and update the rinkeby information.

At the top of the file add require("dotenv").config(); and now update the config like this

const config: HardhatUserConfig = {
solidity: '0.8.4',
networks: {
rinkeby: {
url: process.env.RENKEBY_URL || '',
accounts: process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [],
},
},
};

Are we ready? Let’s deploy it!

npx hardhat run --network rinkeby scripts/deploy.ts

If everything goes well you should see something like this in your console

If you copy that contract address and past it on Etherscan Rinkeby you should be able to see it live! Mine is located here: 0xAf688A3405531e0FC274F6453cD8d3f3dB07D706

Get your contract verified by Etherscan

Source code verification provides transparency for users interacting with smart contracts. By uploading the source code, Etherscan will match the compiled code with that on the blockchain. Just like contracts, a “smart contract” should provide end users with more information on what they are “digitally signing” for and give users an opportunity to audit the code to independently verify that it actually does what it is supposed to do.

So let’s do it. It’s really an easy step because Hardhat provide a specific Plugin to do that: hardhat-etherscan.

Let’s install yarn add --dev @nomiclabs/hardhat-etherscan

and include it in your hardhat.config.ts adding import "@nomiclabs/hardhat-etherscan"; at the end of your imports.

The next step is to register an account on Etherscan and generate an API Key.

Once you have it we need to update our .env file adding this line of code

ETHERSCAN_API_KEY=<PAST_YOU_API_KEY>

and update the hardhat.config.ts adding the Etherscan config needed by the plugin.

The last thing to do is to run the verify task that has been added by the plugin:

npx hardhat verify --network rinkeby <YOUR_CONTRACT_ADDRESS>

The plugin have a lot of configurations and different options so check out their documentation if you want to know more.

If everything goes well you should see something like this in your console

Contract verified by Etherscsan

If you go over the Etherscan contract page again you should see a green checkmark on top of the Contract section. Well done!

Conclusion and next steps

If you want to use this solidity-template to kickstart you next smart contract project just head over to the GitHub repo https://github.com/StErMi/solidity-template and it the “Use this template” green button and boom, you are ready to go!

This is just the start, I’m planning to add more features in time like for example:

Do you have any feedback or do you want to contribute to the Solidity Template? Just create a Pull Request on the GitHub page and let’s discuss it!

Did you like this content? Follow me for more!

--

--

StErMi

#web3 dev + auditor | @SpearbitDAO security researcher, @yAcademyDAO resident auditor, @developer_dao #459, @TheSecureum bootcamp-0, @code4rena warden