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 storedscripts
where all your scripts/tasks will be storedtest
where your smart contract tests will be storedhardhat.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 foldernpx hardhat compile
— To compile the entire project, building your smart contractsnpx hardhat clean
— To clean the cache and delete compiled smart contractsnpx hardhat run —-network <network> script/path
— To run a specific script in a specificnetwork
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:
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
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
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:
- Support to eslint for the typescript testing lib
- Support for hardhat-deploy, an Hardhat Plugin to better manage deployment
- Add support to solidity-coverage plugin
- Add support to hardhat-gas-reporter plugin
- TBD
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!
- GitHub: https://github.com/StErMi
- Twitter: https://twitter.com/StErMi
- Medium: https://medium.com/@stermi
- Dev.to: https://dev.to/stermi