diff --git a/blockchain/contracts/Election.sol b/blockchain/contracts/Election.sol index 49f7ca3..2e5f045 100644 --- a/blockchain/contracts/Election.sol +++ b/blockchain/contracts/Election.sol @@ -1,5 +1,12 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.24; +// Custom errors +error ElectionNotStarted(); +error ElectionEnded(); +error CandidateAlreadyExists(); +error AlreadyVoted(); +error VoterAlreadyRegistered(); +error InvalidCandidateID(); contract Election { string public title; @@ -8,27 +15,23 @@ contract Election { uint public startDate; uint public endDate; - // variable for keeping the number of candidates in election, increases when a new candidate is added uint public candidatesCount; - //defines structure for a candidate struct Candidate { uint id; string name; uint voteCount; + string team; + string image; } - // defines structure for a voter struct Voter { - bool hasVoted; + bool voted; uint candidateId; } - //creates mapping to map a unique candidate Id to a Candidate struct mapping(uint => Candidate) candidates; - // also to tie/map a unique blockchain adresss to a voter mapping(address => Voter) voters; - // event to be emitted when a voter casts a vote event VoteCast(address indexed voter, uint indexed candidateId); constructor( @@ -38,7 +41,7 @@ contract Election { uint _startDate, uint _endDate ) { - require(_startDate < _endDate, "Start date must be before end date"); + if (_startDate >= _endDate) revert ElectionEnded(); title = _title; description = _description; isPublic = _isPublic; @@ -46,22 +49,35 @@ contract Election { endDate = _endDate; } - // Modifier to ensure the election is active modifier onlyWhileOpen() { - require(block.timestamp >= startDate, "Election has not started yet"); - require(block.timestamp <= endDate, "Election has ended"); + if (block.timestamp < startDate) revert ElectionNotStarted(); + if (block.timestamp > endDate) revert ElectionEnded(); _; } - // function to create a new candidate - function addCandidate(string memory _name) public onlyWhileOpen { - // increamented to assign a new id to a candidate + function addCandidate( + string memory _name, + string memory _team, + string memory _image + ) public onlyWhileOpen { + for (uint i = 1; i <= candidatesCount; i++) { + if ( + keccak256(abi.encodePacked(candidates[i].name)) == + keccak256(abi.encodePacked(_name)) + ) { + revert CandidateAlreadyExists(); + } + } candidatesCount++; - // a new candidate struct is created and stored in the candiates mapping - candidates[candidatesCount] = Candidate(candidatesCount, _name, 0); + candidates[candidatesCount] = Candidate( + candidatesCount, + _name, + 0, + _team, + _image + ); } - //function to get all candidates function getCandidates() public view returns (Candidate[] memory) { Candidate[] memory allCandidates = new Candidate[](candidatesCount); for (uint i = 1; i <= candidatesCount; i++) { @@ -70,27 +86,24 @@ contract Election { return allCandidates; } - // Function to add a voter to the election function addVoter(address _voterAddress) public onlyWhileOpen { - require(!voters[_voterAddress].hasVoted, "Voter is already registered"); + require(!voters[_voterAddress].voted, "Voter is already registered"); voters[_voterAddress] = Voter(false, 0); } - //function to get a voter by their Address function getVoter(address _voterAddress) public view returns (bool, uint) { Voter memory voter = voters[_voterAddress]; - return (voter.hasVoted, voter.candidateId); + return (voter.voted, voter.candidateId); } - //function to cast a vote function castVote(uint _candidateId) public onlyWhileOpen { - require(!voters[msg.sender].hasVoted, "You have already voted."); + require(!voters[msg.sender].voted, "You have already voted."); require( _candidateId > 0 && _candidateId <= candidatesCount, "Invalid candidate. Please enter a valid candidate ID" ); - voters[msg.sender].hasVoted = true; + voters[msg.sender].voted = true; voters[msg.sender].candidateId = _candidateId; candidates[_candidateId].voteCount++; diff --git a/blockchain/hardhat.config.ts b/blockchain/hardhat.config.ts index 0ccedcb..e46227e 100644 --- a/blockchain/hardhat.config.ts +++ b/blockchain/hardhat.config.ts @@ -4,6 +4,9 @@ import "dotenv/config"; const config: HardhatUserConfig = { solidity: "0.8.24", + gasReporter: { + enabled: true + }, networks: { sepolia_testnet: { url: `https://ethereum-sepolia-rpc.publicnode.com`, diff --git a/blockchain/test/Election.ts b/blockchain/test/Election.ts index 919f0b6..864a8a8 100644 --- a/blockchain/test/Election.ts +++ b/blockchain/test/Election.ts @@ -1,98 +1,205 @@ import { expect } from "chai"; -import { ethers } from "hardhat"; +import hre from "hardhat"; import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; describe("Election Contract", function () { - // Fixture to deploy the contract and set up initial conditions async function deployElectionFixture() { - const [owner, voter1, voter2] = await ethers.getSigners(); - const Election = await ethers.getContractFactory("Election"); - - const startTime = Math.floor(Date.now() / 1000); - const endTime = startTime + 3600; // Election lasts 1 hour - - const election = await Election.deploy( - "Test Election", - "An election for testing.", - true, - startTime, - endTime - ); - - // Return the variables needed for the tests - return { election, owner, voter1, voter2 }; + const Election = await hre.ethers.getContractFactory("Election"); + const [owner, voter1, voter2] = await hre.ethers.getSigners(); + const title = "Election 2024"; + const description = "Election Description"; + + const startDate = Math.floor(Date.now() / 1000) + 3600; // 1 hour in the future + const endDate = startDate + 86400; // 1 day after start date + + const election = await Election.deploy(title, description, true, startDate, endDate); + + return { election, owner, voter1, voter2, title, description, startDate, endDate }; } - it("should initialize with correct values", async function () { - const { election } = await loadFixture(deployElectionFixture); + it("should prevent actions before election start", async function () { + const { election, startDate } = await loadFixture(deployElectionFixture); + + const block = await hre.ethers.provider.getBlock("latest"); + const currentTimestamp = block?.timestamp ?? 0; + + expect(currentTimestamp).to.be.lessThan(startDate); + + await expect(election.addCandidate("Charlie", "Team C", "charlie.jpg")) + .to.be.revertedWithCustomError(election, "ElectionNotStarted"); // Corrected + }); + + it("should prevent actions after election end", async function () { + const { election, endDate } = await loadFixture(deployElectionFixture); + + const block = await hre.ethers.provider.getBlock("latest"); + const currentTimestamp = block?.timestamp ?? 0; + + const timeToAdvance = (endDate - currentTimestamp) + 3600; + await hre.ethers.provider.send("evm_increaseTime", [timeToAdvance]); + await hre.ethers.provider.send("evm_mine", []); + + await expect(election.addCandidate("David", "Team D", "david.jpg")) + .to.be.revertedWithCustomError(election, "ElectionEnded"); + }); + + it("should prevent adding a candidate after election ends", async function () { + const { election, endDate } = await loadFixture(deployElectionFixture); + + const block = await hre.ethers.provider.getBlock("latest"); + const currentTimestamp = block?.timestamp ?? 0; + + // Fast forward to after the election ends + const timeToAdvance = (endDate - currentTimestamp) + 3600; + await hre.ethers.provider.send("evm_increaseTime", [timeToAdvance]); + await hre.ethers.provider.send("evm_mine", []); + + await expect( + election.addCandidate("Invalid", "Team X", "invalid.jpg") + ).to.be.revertedWithCustomError(election, "ElectionEnded"); + }); + + it("should handle edge cases for adding a candidate", async function () { + const { election, startDate } = await loadFixture(deployElectionFixture); - expect(await election.title()).to.equal("Test Election"); - expect(await election.description()).to.equal("An election for testing."); - expect(await election.isPublic()).to.equal(true); + const block = await hre.ethers.provider.getBlock("latest"); + const currentTimestamp = block?.timestamp ?? 0; + const timeToAdvance = (startDate - currentTimestamp) + 3600; + await hre.ethers.provider.send("evm_increaseTime", [timeToAdvance]); + await hre.ethers.provider.send("evm_mine", []); + + // Now try to add a candidate with the same name to test error + await election.addCandidate("Charlie", "Team C", "charlie.jpg"); + await expect(election.addCandidate("Charlie", "Team C", "charlie.jpg")) + .to.be.revertedWithCustomError(election, "CandidateAlreadyExists"); + + const candidates = await election.getCandidates(); + expect(candidates.length).to.be.greaterThan(0); }); - it("should add a candidate", async function () { - const { election } = await loadFixture(deployElectionFixture); + it("should allow adding candidates and retrieve them after election starts", async function () { + const { election, startDate } = await loadFixture(deployElectionFixture); + + const block = await hre.ethers.provider.getBlock("latest"); + const currentTimestamp = block?.timestamp ?? 0; + + const timeToAdvance = (startDate - currentTimestamp) + 3600; + await hre.ethers.provider.send("evm_increaseTime", [timeToAdvance]); + await hre.ethers.provider.send("evm_mine", []); + + await election.addCandidate("Alice", "Team A", "alice.jpg"); + await election.addCandidate("Bob", "Team B", "bob.jpg"); - await election.addCandidate("Alice"); const candidates = await election.getCandidates(); - expect(candidates.length).to.equal(1); + expect(candidates.length).to.equal(2); expect(candidates[0].name).to.equal("Alice"); + expect(candidates[1].name).to.equal("Bob"); }); - it("should add a voter", async function () { - const { election, voter1 } = await loadFixture(deployElectionFixture); + it("should prevent voting for invalid candidate", async function () { + const { election, startDate, voter1 } = await loadFixture(deployElectionFixture); + const block = await hre.ethers.provider.getBlock("latest"); + const currentTimestamp = block?.timestamp ?? 0; + + const timeToAdvance = (startDate - currentTimestamp) + 3600; + await hre.ethers.provider.send("evm_increaseTime", [timeToAdvance]); + await hre.ethers.provider.send("evm_mine", []); + + await election.addCandidate("Alice", "Team A", "alice.jpg"); await election.addVoter(voter1.address); - const [hasVoted, candidateId] = await election.getVoter(voter1.address); - expect(hasVoted).to.equal(false); - expect(candidateId).to.equal(0); + + await expect(election.connect(voter1).castVote(999)) + .to.be.revertedWith("Invalid candidate. Please enter a valid candidate ID"); }); - it("should allow a voter to cast a vote", async function () { - const { election, voter1 } = await loadFixture(deployElectionFixture); + it("should prevent double voting", async function () { + const { election, startDate, voter1 } = await loadFixture(deployElectionFixture); - await election.addCandidate("Alice"); + const block = await hre.ethers.provider.getBlock("latest"); + const currentTimestamp = block?.timestamp ?? 0; + + const timeToAdvance = (startDate - currentTimestamp) + 3600; + await hre.ethers.provider.send("evm_increaseTime", [timeToAdvance]); + await hre.ethers.provider.send("evm_mine", []); + + await election.addCandidate("Alice", "Team A", "alice.jpg"); await election.addVoter(voter1.address); + // Cast the first vote await election.connect(voter1).castVote(1); - const [hasVoted, candidateId] = await election.getVoter(voter1.address); - expect(hasVoted).to.equal(true); - expect(candidateId).to.equal(1); - - const candidates = await election.getCandidates(); - expect(candidates[0].voteCount).to.equal(1); + // Try to cast another vote + await expect(election.connect(voter1).castVote(1)) + .to.be.revertedWith("You have already voted."); }); - it("should emit VoteCast event when a vote is cast", async function () { - const { election, voter1 } = await loadFixture(deployElectionFixture); + it("should prevent registering voter after election end", async function () { + const { election, endDate, voter1 } = await loadFixture(deployElectionFixture); - await election.addCandidate("Alice"); - await election.addVoter(voter1.address); + const block = await hre.ethers.provider.getBlock("latest"); + const currentTimestamp = block?.timestamp ?? 0; - await expect(election.connect(voter1).castVote(1)) - .to.emit(election, "VoteCast") - .withArgs(voter1.address, 1); + const timeToAdvance = (endDate - currentTimestamp) + 3600; + await hre.ethers.provider.send("evm_increaseTime", [timeToAdvance]); + await hre.ethers.provider.send("evm_mine", []); + + await expect(election.addVoter(voter1.address)) + .to.be.revertedWithCustomError(election, "ElectionEnded"); }); - it("should not allow voting twice", async function () { - const { election, voter1 } = await loadFixture(deployElectionFixture); + it("should prevent adding a voter before election starts", async function () { + const { election, startDate, voter1 } = await loadFixture(deployElectionFixture); - await election.addCandidate("Alice"); + // Attempt to add a voter before the election starts + await expect(election.addVoter(voter1.address)) + .to.be.revertedWithCustomError(election, "ElectionNotStarted"); + }); + + it("Should return a voter", async function () { + const { election, startDate, voter1 } = await loadFixture(deployElectionFixture); + + // Advance time to start the election + const block = await hre.ethers.provider.getBlock("latest"); + const currentTimestamp = block?.timestamp ?? 0; + const timeToAdvance = (startDate - currentTimestamp) + 3600; + await hre.ethers.provider.send("evm_increaseTime", [timeToAdvance]); + await hre.ethers.provider.send("evm_mine", []); + + // Add a candidate and a voter + await election.addCandidate("Alice", "Team A", "alice.jpg"); await election.addVoter(voter1.address); + // Cast a vote await election.connect(voter1).castVote(1); - await expect(election.connect(voter1).castVote(1)).to.be.revertedWith("You have already voted."); + // Retrieve voter information + const [voted, candidateId] = await election.getVoter(voter1.address); + + // Check the voter details + expect(voted).to.be.true; + expect(candidateId).to.equal(1); }); - it("should not allow voting for a non-existent candidate", async function () { - const { election, voter1 } = await loadFixture(deployElectionFixture); + it("should handle invalid dates in the constructor", async function () { + const Election = await hre.ethers.getContractFactory("Election"); - await election.addCandidate("Alice"); - await election.addVoter(voter1.address); + const startDate = Math.floor(Date.now() / 1000) + 3600; // 1 hour in the future + const endDate = startDate - 1000; // End date before start date + + await expect( + Election.deploy("Election 2024", "Invalid Dates", true, startDate, endDate) + ).to.be.revertedWithCustomError(Election, "ElectionEnded"); + }); + + it("should handle start date equal to end date", async function () { + const Election = await hre.ethers.getContractFactory("Election"); + + const startDate = Math.floor(Date.now() / 1000) + 3600; + const endDate = startDate; // Equal start and end date - await expect(election.connect(voter1).castVote(2)).to.be.revertedWith("Invalid candidate. Please enter a valid candidate ID"); + await expect( + Election.deploy("Election 2024", "Start date equals end date", true, startDate, endDate) + ).to.be.revertedWithCustomError(Election, "ElectionEnded"); }); }); diff --git a/blockchain/test/ElectionFactory.ts b/blockchain/test/ElectionFactory.ts index 59f4779..ec479ab 100644 --- a/blockchain/test/ElectionFactory.ts +++ b/blockchain/test/ElectionFactory.ts @@ -77,7 +77,7 @@ describe("ElectionFactory Contract", function () { await electionFactory.connect(owner).deleteElection(0); const elections = await electionFactory.getElections(); - expect(elections[0]).to.equal(ethers.ZeroAddress); // Use ethers.constants.AddressZero to check the zero address + expect(elections[0]).to.equal(hre.ethers.ZeroAddress); // Use ethers.constants.AddressZero to check the zero address }); it("Should return the correct total number of elections", async function () {