Getting Started with Hardhat: Complete Setup Guide and Voting Contract Tutorial

What is Hardhat?
Hardhat is a comprehensive Ethereum development environment that simplifies the process of building, testing, and deploying smart contracts. It provides developers with a complete toolkit including a built-in Solidity compiler, testing framework, debugging tools, and a local blockchain network for development. The platform is designed around tasks and plugins, making it highly extensible and customizable for different project needs.
Setting Up Hardhat: Step-by-Step Installation
Prerequisites
Before installing Hardhat, ensure you have the following requirements:
Node.js v18 or later installed on your system
npm (Node Package Manager) or yarn for dependency management
A code editor like Visual Studio Code (recommended)
Basic familiarity with JavaScript and command line operations
Installation Process
Step 1: Create Project Directory
mkdir voting-hardhat-project
cd voting-hardhat-project
Step 2: Initialize npm Project
npm init -y
Step 3: Install Hardhat
npm install --save-dev hardhat
Step 4: Initialize Hardhat Project
npx hardhat init
When prompted, select "Create a JavaScript project" and follow the setup wizard. This will create the basic project structure and install necessary dependencies.
Step 5: Install Additional Dependencies
npm install --save-dev @nomicfoundation/hardhat-toolbox
Project Structure Overview
After initialization, your Hardhat project will have the following structure:
textvoting-hardhat-project/
├── contracts/ # Smart contract files (.sol)
├── scripts/ # Deployment scripts
├── test/ # Test files
├── hardhat.config.js # Hardhat configuration
├── package.json # Project dependencies
└── README.md # Project documentation
Creating a Simple Voting Contract
Now let's create a practical voting smart contract that demonstrates core blockchain concepts.
Contract Code
Create a new file contracts/SimpleVoting.sol
:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SimpleVoting {
struct Candidate {
uint id;
string name;
uint voteCount;
}
mapping(uint => Candidate) public candidates;
mapping(address => bool) public hasVoted;
uint public candidatesCount;
address public owner;
event VoteCast(address indexed voter, uint indexed candidateId);
constructor() {
owner = msg.sender;
addCandidate("Alice");
addCandidate("Bob");
addCandidate("Charlie");
}
function addCandidate(string memory _name) private {
candidatesCount++;
candidates[candidatesCount] = Candidate(candidatesCount, _name, 0);
}
function vote(uint _candidateId) public {
require(!hasVoted[msg.sender], "You have already voted");
require(_candidateId > 0 && _candidateId <= candidatesCount, "Invalid candidate ID");
hasVoted[msg.sender] = true;
candidates[_candidateId].voteCount++;
emit VoteCast(msg.sender, _candidateId);
}
function getCandidate(uint _candidateId) public view returns (uint, string memory, uint) {
require(_candidateId > 0 && _candidateId <= candidatesCount, "Invalid candidate ID");
Candidate memory candidate = candidates[_candidateId];
return (candidate.id, candidate.name, candidate.voteCount);
}
function getWinner() public view returns (string memory, uint) {
uint highestVotes = 0;
uint winnerId = 0;
for (uint i = 1; i <= candidatesCount; i++) {
if (candidates[i].voteCount > highestVotes) {
highestVotes = candidates[i].voteCount;
winnerId = i;
}
}
return (candidates[winnerId].name, highestVotes);
}
}
This voting contract includes:
Candidate Management: Stores candidate information with vote counts
Voting Prevention: Ensures each address can only vote once
Vote Tracking: Maintains accurate vote tallies
Winner Determination: Identifies the candidate with the most votes
Event Logging: Emits events for transparency
Writing Tests
Create a comprehensive test file test/SimpleVoting.js
:
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("SimpleVoting", function () {
let SimpleVoting;
let simpleVoting;
let owner;
let addr1;
let addr2;
beforeEach(async function () {
SimpleVoting = await ethers.getContractFactory("SimpleVoting");
[owner, addr1, addr2] = await ethers.getSigners();
simpleVoting = await SimpleVoting.deploy();
await simpleVoting.waitForDeployment();
});
describe("Deployment", function () {
it("Should set the right owner", async function () {
expect(await simpleVoting.owner()).to.equal(owner.address);
});
it("Should create 3 initial candidates", async function () {
expect(await simpleVoting.candidatesCount()).to.equal(3);
const alice = await simpleVoting.getCandidate(1);
const bob = await simpleVoting.getCandidate(2);
const charlie = await simpleVoting.getCandidate(3);
expect(alice[1]).to.equal("Alice");
expect(bob[1]).to.equal("Bob");
expect(charlie[1]).to.equal("Charlie");
});
});
describe("Voting", function () {
it("Should allow voting for valid candidates", async function () {
await simpleVoting.connect(addr1).vote(1);
const alice = await simpleVoting.getCandidate(1);
expect(alice[2]).to.equal(1);
expect(await simpleVoting.hasVoted(addr1.address)).to.be.true;
});
it("Should prevent double voting", async function () {
await simpleVoting.connect(addr1).vote(1);
await expect(
simpleVoting.connect(addr1).vote(2)
).to.be.revertedWith("You have already voted");
});
it("Should reject invalid candidate IDs", async function () {
await expect(
simpleVoting.connect(addr1).vote(0)
).to.be.revertedWith("Invalid candidate ID");
});
});
describe("Winner determination", function () {
it("Should correctly determine winner", async function () {
await simpleVoting.connect(addr1).vote(1); // Alice
await simpleVoting.connect(addr2).vote(1); // Alice
await simpleVoting.connect(owner).vote(2); // Bob
const [winnerName, winnerVotes] = await simpleVoting.getWinner();
expect(winnerName).to.equal("Alice");
expect(winnerVotes).to.equal(2);
});
});
});
Creating Deployment Script
Create the deployment script scripts/deploy.js
:
const { ethers } = require("hardhat");
async function main() {
console.log("Deploying SimpleVoting contract...");
const SimpleVoting = await ethers.getContractFactory("SimpleVoting");
const simpleVoting = await SimpleVoting.deploy();
await simpleVoting.waitForDeployment();
console.log("SimpleVoting contract deployed to:", await simpleVoting.getAddress());
// Display initial candidates
console.log("\nInitial candidates:");
for (let i = 1; i <= 3; i++) {
const candidate = await simpleVoting.getCandidate(i);
console.log(`ID: ${candidate[0]}, Name: ${candidate[1]}, Votes: ${candidate[2]}`);
}
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
Compilation and Testing
Step 1: Compile the Contract
npx hardhat compile
Step 2: Run Tests
npx hardhat test
Step 3: Deploy to Local Network
hardhat run scripts/deploy.js
Deploying to Testnet
To deploy to a live testnet like Sepolia, you need to configure your hardhat.config.js
file.
Configuration Setup
require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config();
module.exports = {
solidity: "0.8.0",
networks: {
sepolia: {
url: process.env.SEPOLIA_RPC_URL,
accounts: [process.env.PRIVATE_KEY],
chainId: 11155111,
},
},
};
Environment Variables
Create a .env
file in your project root:
SEPOLIA_RPC_URL=https://sepolia.infura.io/v3/YOUR_INFURA_KEY
PRIVATE_KEY=your_wallet_private_key
Deploy to Sepolia Testnet
Before deployment, ensure you have:
Sepolia ETH for gas fees (get from faucets like sepoliafaucet.com)
RPC URL from providers like Infura or Alchemy
MetaMask wallet configured for Sepolia testnet
Deploy with:
hardhat run scripts/deploy.js --network sepolia
Interacting with Your Contract
Once deployed, you can interact with your voting contract using Hardhat console:
hardhat console --network sepolia
Example interactions:
const SimpleVoting = await ethers.getContractFactory("SimpleVoting");
const contract = SimpleVoting.attach("YOUR_CONTRACT_ADDRESS");
// Vote for a candidate
await contract.vote(1);
// Check winner
const [winner, votes] = await contract.getWinner();
console.log(`Winner: ${winner} with ${votes} votes`);
Best Practices and Security Considerations
Testing First: Always write comprehensive tests before deployment
Testnet Deployment: Test on testnets before mainnet deployment
Security Audits: Consider professional audits for production contracts
Gas Optimization: Optimize contract code for gas efficiency
Access Control: Implement proper permission systems
Event Logging: Use events for transparency and tracking
Common Troubleshooting
Network Issues: Ensure your network configuration matches the target blockchain
Gas Estimation: Check sufficient ETH balance for transaction fees
Version Compatibility: Use compatible Solidity and Hardhat versions
RPC Connection: Verify your RPC URL is correct and accessible
This comprehensive guide provides everything you need to get started with Hardhat and deploy your first voting smart contract. The combination of local development, thorough testing, and testnet deployment ensures a robust development workflow for blockchain applications.
Subscribe to my newsletter
Read articles from Vairamuthu directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
