Skip to main content

Creating a voting contract

In this tutorial, we will create a voting contract that allows users to register themsleves and vote for a candidate. The contract will keep track of the votes for each candidate and the total number of votes. The contract will also keep track of the number of registered users.

Creating types for the contract

We will define two types for this contract. The first one is candidate which will be used to store the name of the candidate as well as the address.

//contracts/Types.sol
struct Candidate {
string name;
address candidateAddress;
}

The second type is voting result. This type will be used when we call get voting result function which holds the candidates' info as well as the total number of votes.

//contracts/Types.sol
struct Results {
string name;
uint256 voteCount;
address candidateAddress;
}

Import types

In order to use the types we just created, we need to import them in the contract.

//contracts/Ballot.sol
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.17;

import "./Types.sol";

Creating the contract

Preparing the contract

We will create a contract called Ballot that will have hold the following variables in order for us to track the number of registered users and the number of votes for each candidate.

contract Ballot {
// cadiidate list
Types.Candidate[] public candidates;
mapping(address => uint256) public votesReceived;
// voter list
address[] public voters;
mapping(address => bool) public voterStatus;
// total votes
uint256 public totalVotes;
// owner of the contract
address public owner;
// start time of the voting
uint256 public startTime;
// end time of the voting
uint256 public endTime;

In order for our front end to get the latest update of the contract, we will emit an event whenever a user registers, reset or votes.

event Vote(address indexed voter, uint256 indexed candidateIndex);
event AddCandidate(uint256 indexed candidateIndex);
event Reset();

We will initialize the contract with the ending time of the voting and the owner of the contract.

constructor(uint256 _endTime) {
owner = msg.sender;
totalVotes = 0;
startTime = block.timestamp;
endTime = block.timestamp + _endTime;
}

We will create a helper function call isEnded that will return true if the voting has ended.

// is the voting ended?
function isEnded() public view returns (bool) {
return block.timestamp > endTime;
}

The reset function will be called by the owner of the contract in order to reset the contract.

// reset the voting
function reset(uint256 _endTime) public {
require(msg.sender == owner, "Only owner can reset the voting");
for (uint256 i = 0; i < candidates.length; i++) {
votesReceived[candidates[i].candidateAddress] = 0;
}
// reset the candidate array
delete candidates;
// reset the voter list
for (uint256 i = 0; i < voters.length; i++) {
voterStatus[voters[i]] = false;
}
delete voters;
// reset the total votes
totalVotes = 0;
// reset the start time
startTime = block.timestamp;
endTime = startTime + _endTime;
emit Reset();
}

Registering the candidate

Every one can register as a candidate by calling the registerCandidate function.

function registerCandidate(string memory _name) public {
require(!isEnded(), "Voting is ended");
candidates.push(Types.Candidate(_name, msg.sender));
votesReceived[msg.sender] = 0;
emit AddCandidate(candidates.length - 1);
}

Voting for a candidate

Every one can vote for a candidate by calling the vote function.

// vote for a candidate
function vote(uint256 candidateIndex) public {
require(!isEnded(), "Voting is ended");
require(!voterStatus[msg.sender], "Already voted");
require(candidateIndex < candidates.length, "Invalid candidate index");
require(voterStatus[msg.sender] == false, "Already voted");

votesReceived[candidates[candidateIndex].candidateAddress]++;
voterStatus[msg.sender] = true;
voters.push(msg.sender);
totalVotes++;
emit Vote(msg.sender, candidateIndex);
}

Getting the voting result

The getVotingResult function will return the voting result.

// get the results
function getResults() public view returns (Types.Results[] memory) {
Types.Results[] memory results = new Types.Results[](candidates.length);
for (uint256 i = 0; i < candidates.length; i++) {
results[i] = Types.Results(
candidates[i].name,
votesReceived[candidates[i].candidateAddress],
candidates[i].candidateAddress
);
}
return results;
}

Complete code

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.17;

import "./Types.sol";

contract Ballot {
// cadiidate list
Types.Candidate[] public candidates;
mapping(address => uint256) public votesReceived;
// voter list
address[] public voters;
mapping(address => bool) public voterStatus;
// total votes
uint256 public totalVotes;
// owner of the contract
address public owner;
// start time of the voting
uint256 public startTime;
// end time of the voting
uint256 public endTime;

// event
event Vote(address indexed voter, uint256 indexed candidateIndex);
event AddCandidate(uint256 indexed candidateIndex);
event Reset();

constructor(uint256 _endTime) {
owner = msg.sender;
totalVotes = 0;
startTime = block.timestamp;
endTime = block.timestamp + _endTime;
}

// is the voting ended?
function isEnded() public view returns (bool) {
return block.timestamp > endTime;
}

// reset the voting
function reset(uint256 _endTime) public {
require(msg.sender == owner, "Only owner can reset the voting");
for (uint256 i = 0; i < candidates.length; i++) {
votesReceived[candidates[i].candidateAddress] = 0;
}
// reset the candidate array
delete candidates;
// reset the voter list
for (uint256 i = 0; i < voters.length; i++) {
voterStatus[voters[i]] = false;
}
delete voters;
// reset the total votes
totalVotes = 0;
// reset the start time
startTime = block.timestamp;
endTime = startTime + _endTime;
emit Reset();
}

// register a candidate
function registerCandidate(string memory _name) public {
require(!isEnded(), "Voting is ended");
candidates.push(Types.Candidate(_name, msg.sender));
votesReceived[msg.sender] = 0;
emit AddCandidate(candidates.length - 1);
}

// vote for a candidate
function vote(uint256 candidateIndex) public {
require(!isEnded(), "Voting is ended");
require(!voterStatus[msg.sender], "Already voted");
require(candidateIndex < candidates.length, "Invalid candidate index");
require(voterStatus[msg.sender] == false, "Already voted");

votesReceived[candidates[candidateIndex].candidateAddress]++;
voterStatus[msg.sender] = true;
voters.push(msg.sender);
totalVotes++;
emit Vote(msg.sender, candidateIndex);
}

// get the results
function getResults() public view returns (Types.Results[] memory) {
Types.Results[] memory results = new Types.Results[](candidates.length);
for (uint256 i = 0; i < candidates.length; i++) {
results[i] = Types.Results(
candidates[i].name,
votesReceived[candidates[i].candidateAddress],
candidates[i].candidateAddress
);
}
return results;
}
}

Tests

import { time } from "@nomicfoundation/hardhat-network-helpers";
import { expect } from "chai";
import { ethers } from "hardhat";

describe("Ballot", function () {
it("should be able to vote", async function () {
const [owner, voter1, voter2, candidate1, candidate2] =
await ethers.getSigners();
const Ballot = await ethers.getContractFactory("Ballot");
const ballot = await Ballot.deploy(3000);
await ballot.deployed();

// register candidates
await ballot.connect(candidate1).registerCandidate("candidate1");
await ballot.connect(candidate2).registerCandidate("candidate2");

await ballot.connect(voter1).vote(0);
await ballot.connect(voter2).vote(1);

const results = await ballot.getResults();
expect(results).to.have.length(2);
expect(results[0].candidateAddress).to.equal(candidate1.address);
expect(results[0].voteCount).to.equal(1);
expect(results[1].candidateAddress).to.equal(candidate2.address);
expect(results[1].voteCount).to.equal(1);

await ballot.connect(owner).reset(3000);
expect(await ballot.getResults()).to.have.length(0);

await ballot.connect(candidate1).registerCandidate("candidate1");
await ballot.connect(voter1).vote(0);
});

it("Should not be able to register when voting is ended", async function () {
const [owner, voter1, voter2, candidate1, candidate2] =
await ethers.getSigners();
const Ballot = await ethers.getContractFactory("Ballot");
const ballot = await Ballot.deploy(3000);
await ballot.deployed();

await time.increase(4000);

await expect(
ballot.connect(voter1).registerCandidate("alice")
).to.be.revertedWith("Voting is ended");
});

it("Should not be able to vote when voting is ended", async function () {
const [owner, voter1, voter2, candidate1, candidate2] =
await ethers.getSigners();
const Ballot = await ethers.getContractFactory("Ballot");
const ballot = await Ballot.deploy(3000);
await ballot.deployed();

await ballot.connect(candidate1).registerCandidate("candidate1");

await time.increase(4000);

await expect(ballot.connect(voter1).vote(0)).to.be.revertedWith(
"Voting is ended"
);
});
});