Build a Decentralized Voting App with Solidity and Hardhat

Hardhat

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