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"
);
});
});