Welcome to the world of decentralized application development! If you’re looking to get your hands dirty with Solidity, Hardhat is the perfect tool to streamline your workflow. It provides a flexible, fast, and feature-rich environment for compiling, testing, and deploying your Ethereum smart contracts.
In this tutorial, we’ll build a complete, albeit simple, decentralized voting system from scratch. You’ll learn how to set up a Hardhat project, write a Solidity smart contract, test its logic, and deploy it to a local blockchain.
Prerequisites
Before we start, make sure you have the following installed:
- Node.js (v16 or later)
- A code editor like VS Code
- Basic understanding of JavaScript and the command line
1. Initialize Your Hardhat Project
First, let’s create a new folder for our project and initialize it with npm. Open your terminal and run these commands:
mkdir hardhat-voting-app
cd hardhat-voting-app
npm init -y
Next, install Hardhat as a development dependency:
npm install --save-dev hardhat
Now, run Hardhat’s project initializer:
npx hardhat
You’ll be prompted with a few questions. Choose the following options:
Create a JavaScript project
- Accept the default project root
- Answer
y
to adding a.gitignore
This will create a basic Hardhat project structure with contracts
, scripts
, and test
directories.
2. Write the Voting Smart Contract
Now for the core logic. Create a new file inside the contracts
directory named Voting.sol
.
Delete the sample Lock.sol
file and paste the following code into Voting.sol
:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
contract Voting {
// Defines a new data type 'Candidate' with a name and vote count.
struct Candidate {
string name;
uint256 voteCount;
}
// A dynamic array to store all candidates. 'public' makes it readable from outside.
Candidate[] public candidates;
// A mapping to track which addresses have already voted.
mapping(address => bool) public hasVoted;
// An event that gets emitted every time a vote is cast.
event Voted(address indexed voter, string candidateName);
// The constructor runs once when the contract is deployed.
// It takes an array of candidate names and initializes the candidates list.
constructor(string[] memory _candidateNames) {
for (uint i = 0; i < _candidateNames.length; i++) {
candidates.push(Candidate({
name: _candidateNames[i],
voteCount: 0
}));
}
}
// Allows a user to cast their vote.
function vote(uint _candidateIndex) public {
// 'require' checks for conditions. If false, it reverts the transaction.
require(!hasVoted[msg.sender], "You have already cast your vote.");
require(_candidateIndex < candidates.length, "Invalid candidate index.");
// Record that the sender has voted.
hasVoted[msg.sender] = true;
// Increment the candidate's vote count.
candidates[_candidateIndex].voteCount++;
// Emit the 'Voted' event to the blockchain.
emit Voted(msg.sender, candidates[_candidateIndex].name);
}
// A view function to get the vote count for a specific candidate.
function getVoteCount(uint _candidateIndex) public view returns (uint256) {
require(_candidateIndex < candidates.length, "Invalid candidate index.");
return candidates[_candidateIndex].voteCount;
}
}
3. Compile Your Contract
Compiling translates your Solidity code into bytecode that the Ethereum Virtual Machine (EVM) can understand.
npx hardhat compile
If everything is correct, you’ll see a Compilation finished successfully
message. This creates an artifacts
directory containing the compiled contract data.
4. Test Your Contract Logic
Testing is a critical step in smart contract development. Hardhat uses the powerful Chai and Mocha testing frameworks. Let’s write a test to ensure our contract behaves as expected.
Delete the sample Lock.js
in the test
folder and create a new file named Voting.test.js
. Add the following code:
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("Voting Contract", function () {
let Voting;
let voting;
let owner;
let addr1;
const candidateNames = ["Alice", "Bob", "Charlie"];
// Before each test, deploy a new instance of the contract
beforeEach(async function () {
[owner, addr1] = await ethers.getSigners();
const VotingFactory = await ethers.getContractFactory("Voting");
voting = await VotingFactory.deploy(candidateNames);
});
it("Should initialize with the correct candidates", async function () {
const alice = await voting.candidates(0);
expect(alice.name).to.equal("Alice");
expect(alice.voteCount).to.equal(0);
});
it("Should allow a user to vote", async function () {
// Vote for candidate at index 1 (Bob)
await voting.connect(addr1).vote(1);
// Check if Bob's vote count increased to 1
const bob = await voting.candidates(1);
expect(bob.voteCount).to.equal(1);
});
it("Should prevent a user from voting twice", async function () {
await voting.connect(addr1).vote(0); // First vote
// Expect the second vote to be reverted
await expect(
voting.connect(addr1).vote(1)
).to.be.revertedWith("You have already cast your vote.");
});
it("Should emit a Voted event", async function () {
await expect(voting.connect(addr1).vote(2))
.to.emit(voting, "Voted")
.withArgs(addr1.address, "Charlie"); // Check if event was emitted with correct arguments
});
});
Now, run the test:
npx hardhat test
You should see all tests passing. This confirms your contract’s logic is solid!
Conclusion and Next Steps
Congratulations! You have successfully built, tested, and deployed a decentralized voting smart contract using Hardhat. You’ve learned the fundamental workflow of a professional Web3 developer.
Also Read: Introduction to Smart Contract Development with Solidity