diff --git a/README.md b/README.md index feda28f..ccfb08a 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ To setup the application follow these steps: 3. run `npm run prepare` 4. Write a `.env` file in the root of the project and configure: * `SEPOLIA_URL` You can find the data in your Alchemy account, after you create an app there; - * `ALCHEMY_API_KEY` You can find the data in your Alchemy account; + * `ALCHEMY_PRIVATE_KEY` You can find the data in your Alchemy account; * `REPORT_GAS` enable or disable the gas report on smart contracts unit tests executions; * `NODE_ENV` set `development` for your local machine; @@ -77,8 +77,39 @@ To deploy to a specific network (e.g. mainnet, sepora), the network must be conf For the local network the parameter to pass is `localhost`, there is no need to configure the local network. +## Emulate the whole process using the scrips -# Donations +### Prerequisites + +* edit the scripts mocks file: `election-scripts/__mocks__.ts`; + * edit the municipality election contract data, in particular registrationStart and registrationEnd are timestamps in seconds; + * edit the data of the parties and candidates as you prefer; + +### 1. The Public Authority / Admin creates the DECs Registry +For the creation of the registry we deploy the DECs Registry smart contract using ignition: + +`npm run deploy-contract Registry localhost`; + +### 2. The Public Authority / Admin creates the EOA for the Voter +Execute the `create-voter` scripts and take note of the resulting `address` and `privateKey`: + +`npx hardhat run election-scripts/create-voter-eoa.ts` + +### 3. The Public Authority / Admin creates the DEC for the Voter and register the DEC into the DECs Registry +[TO DO] + +### 4. The Public Authority / Admin creates a Municipality Election +At this point we have the EOA credentials and the DEC for our voters, and the DECs are registered on the DECs Registry. It's time to create an election: as an example we implemented a smart contract for a municipality election, that elects the major and the council. + +Now it's time to deploy the smart contract election and register parties, councilor and major candidates, parties coalitions in the municipality election contract, run the command: + +`npx hardhat run election-scripts/create-election.ts` + +Please note that to make the registration of parties and coalition working, the functions must be called in the registration period set before. + + + +## Donations Support this project and offer me a crypto-coffee!! ![wallet](docs/assets/wallet_address.png) diff --git a/contracts/CountryElection.sol b/contracts/CountryElection.sol index 742d66d..7cf923c 100644 --- a/contracts/CountryElection.sol +++ b/contracts/CountryElection.sol @@ -8,7 +8,12 @@ import "./Election.sol"; /// @custom:experimental This is an experimental contract. contract CountryElection is Election { - constructor(uint256 _electionStart, uint256 _electionEnd) Election(_electionStart, _electionEnd) { + constructor( + string memory _name, + uint256 _registrationStart, + uint256 _registrationEnd, + uint8 _votingPoints + ) Election(_name, _registrationStart, _registrationEnd, _votingPoints) { } diff --git a/contracts/DEC.sol b/contracts/DEC.sol index d18b581..84141ac 100644 --- a/contracts/DEC.sol +++ b/contracts/DEC.sol @@ -30,35 +30,35 @@ contract DEC { _; } - function setTaxCode(bytes memory _taxCode) public onlyOwner { + function setTaxCode(bytes memory _taxCode) external onlyOwner { taxCode = _taxCode; } - function getTaxCode() public view returns (bytes memory) { + function getTaxCode() external view returns (bytes memory) { return taxCode; } - function setMunicipality(bytes memory _municipality) public onlyOwner { + function setMunicipality(bytes memory _municipality) external onlyOwner { municipality = _municipality; } - function getMunicipality() public view returns (bytes memory) { + function getMunicipality() external view returns (bytes memory) { return municipality; } - function setRegion(bytes memory _region) public onlyOwner { + function setRegion(bytes memory _region) external onlyOwner { region = _region; } - function getRegion() public view returns (bytes memory) { + function getRegion() external view returns (bytes memory) { return region; } - function setCountry(bytes memory _country) public onlyOwner { + function setCountry(bytes memory _country) external onlyOwner { country = _country; } - function getCountry() public view returns (bytes memory) { + function getCountry() external view returns (bytes memory) { return country; } diff --git a/contracts/DECsRegistry.sol b/contracts/DECsRegistry.sol index b10f24d..8bd3bc9 100644 --- a/contracts/DECsRegistry.sol +++ b/contracts/DECsRegistry.sol @@ -33,17 +33,17 @@ contract DECsRegistry { } /// @notice DECs REgistry name setter function - function setName(string memory _name) public onlyOwner { + function setName(string memory _name) external onlyOwner { name = _name; } /// @notice DECs REgistry name getter function - function getName() public view returns (string memory) { + function getName() external view returns (string memory) { return name; } /// @notice this function is used by the third party authority to register a Voter's DEC in the registry - function registerDEC(address dec, address voter) public onlyOwner { + function registerDEC(address dec, address voter) external onlyOwner { require( registry[voter] == address(0), "The Voter's DEC has been already registered" @@ -54,7 +54,7 @@ contract DECsRegistry { } /// @notice this function returns an encrypted DEC in order to check if a Voter has the voting rights - function getDEC(address voter) public view returns (address) { + function getDEC(address voter) external view returns (address) { require( registry[voter] != address(0), "The Voter don't have a registered DEC" @@ -66,7 +66,7 @@ contract DECsRegistry { function hasVoterAlreadyVoted( address voter, address election - ) public view returns (bool) { + ) external view returns (bool) { for (uint i = 0; i < electoralStamps[voter].length; i++) { if (electoralStamps[voter][i] == election) { return true; @@ -76,7 +76,7 @@ contract DECsRegistry { } /// @notice this function put the election stamp on the Voter's stamps list after the vote - function stamps(address election, address voter) public onlyOwner { + function stamps(address election, address voter) external onlyOwner { electoralStamps[voter].push(election); emit DECStamped(election, voter); return; diff --git a/contracts/Election.sol b/contracts/Election.sol index 4f20789..beb5e67 100644 --- a/contracts/Election.sol +++ b/contracts/Election.sol @@ -7,15 +7,29 @@ pragma solidity ^0.8.24; /// @custom:experimental This is an experimental contract. contract Election { address public owner; - uint256 private electionStart; - uint256 private electionEnd; + string public name; + uint256 public electionStart; + uint256 public electionEnd; + uint256 public registrationStart; + uint256 public registrationEnd; + uint8 private votingPoints; mapping (uint256 => string) private ballotBox; // change the data types later mapping (uint256 => string) results; // change the data types later - constructor(uint256 _electionStart, uint256 _electionEnd) { + constructor( + string memory _name, + uint256 _registrationStart, + uint256 _registrationEnd, + uint8 _votingPoints + ) { + require(_registrationStart < _registrationEnd, "The registration start date can't be equal or after the registration end date"); + require(_votingPoints > 19, "It is not possible to assing less that 20 voting points for the election"); + owner = msg.sender; - electionStart = _electionStart; - electionEnd = _electionEnd; + name = _name; + registrationStart = _registrationStart; + registrationEnd = _registrationEnd; + votingPoints = _votingPoints; } modifier onlyOwner() { @@ -28,18 +42,29 @@ contract Election { return (block.timestamp >= electionStart && block.timestamp <= electionEnd); } + /// @notice this function checks if the transaction occours when the registration are open + function isIvokedInRegistrationPeriod() private view returns (bool) { + return (block.timestamp >= registrationStart && block.timestamp <= registrationEnd); + } + /// @notice this function checks if the transaction occours after the election is closed function isElectionClosed() private view returns (bool) { return block.timestamp > electionEnd; } - function setElectionStart(uint256 _electionStart) public onlyOwner { + /// @notice this function checks if the transaction occours after the registration is closed + function isRegistrationClosed() private view returns (bool) { + return block.timestamp > registrationEnd; + } + + function setElectionStart(uint256 _electionStart) external onlyOwner { + require(block.timestamp > registrationEnd, "Elections can't start before the end of the registration process"); require(!isIvokedInElectionPeriod(), "Elections have already started, it's too late for changing the start of the elections"); require(!isElectionClosed(), "Elections are closed, it's not possible to change the start of the elections"); electionStart = _electionStart; } - function getElectionStart() public view returns (uint256) { + function getElectionStart() external view returns (uint256) { return electionStart; } @@ -49,19 +74,38 @@ contract Election { electionEnd = _electionEnd; } - function getElectionEnd() public view returns (uint256) { + function getElectionEnd() external view returns (uint256) { return electionEnd; } + function setRegistrationStart(uint256 _registrationStart) external onlyOwner { + require(!isIvokedInRegistrationPeriod(), "Registrations have already started, it's too late for changing the start of the registration"); + require(!isRegistrationClosed(), "Registration are closed, it's not possible to change the start of the registration"); + registrationStart = _registrationStart; + } + + function getRegistrationStart() external view returns (uint256) { + return registrationStart; + } + + function setRegistrationEnd(uint256 _registrationEnd) public onlyOwner { + require(!isIvokedInRegistrationPeriod(), "Registrations have already started, it's too late for changing the end of the registration"); + require(!isRegistrationClosed(), "Registrations are closed, it's not possible to change the end of the registration"); + registrationEnd = _registrationEnd; + } + + function getRegistrationEnd() external view returns (uint256) { + return registrationEnd; + } + /// @notice this function collects the ballots - function vote() public view { + function vote() external view { require(isIvokedInElectionPeriod(), "Elections are not open"); require(!isElectionClosed(), "Elections are closed"); } /// @notice this function calculates the elections results - function scrutiny() public view onlyOwner { - require(!isIvokedInElectionPeriod(), "Elections are in progress, it's not possible to calculate the results"); + function scrutiny() external view onlyOwner { require(isElectionClosed(), "Scrutiny is possible only after the elections end"); } } \ No newline at end of file diff --git a/contracts/MunicipalityElection.sol b/contracts/MunicipalityElection.sol index 2aa5059..34bdd29 100644 --- a/contracts/MunicipalityElection.sol +++ b/contracts/MunicipalityElection.sol @@ -5,11 +5,196 @@ import "./Election.sol"; /// @title The Municipality Election smart contract /// @author Christian Palazzo +/// @notice This is a kind of Election implementation of the Election mother contract. /// @custom:experimental This is an experimental contract. contract MunicipalityElection is Election { + struct Candidate { + string name; + string candidatesFor; // major or councilior + uint256 points; + } + + /// @dev the string key of the map correspond tho the party name registered in the parties mapping + struct Coalition { + Candidate majorCandidate; + string[] parties; + } + + /// @dev the of the mapping is the party name + mapping(string => Candidate[]) private parties; + uint256 partiesLength; + + Coalition[] private coalitions; + + /// @dev this is a mapping used in private function + mapping(string => bool) private isCounciliorCandidateInArray; + Candidate[] private counciliorCandidatesArray; + + /// @dev this is a mapping used in private function + mapping(string => bool) private partyExistsInCoalition; + + string public municipality; + string public region; + string public country; + + constructor( + string memory _name, + string memory _municipality, + string memory _region, + string memory _country, + uint256 _registrationStart, + uint256 _registrationEnd, + uint8 _votingPoints + ) Election(_name, _registrationStart, _registrationEnd, _votingPoints) { + municipality = _municipality; + region = _region; + country = _country; + } + + modifier isRegistrationPeriod() { + require( + block.timestamp >= registrationStart && + block.timestamp <= registrationEnd, + "This function can only be invoked during the registration period" + ); + _; + } + + function getCandidatesByParty( + string memory partyName + ) external view returns (string[] memory) { + Candidate[] memory candidateList = parties[partyName]; + string[] memory candidateNames = new string[](5); + + // it retrieves the names of the candidates from the mapping + for (uint i = 0; i < candidateList.length; i++) { + candidateNames[i] = candidateList[i].name; + } + + return candidateNames; + } + + function getCoalition( + uint256 index + ) external view returns (Candidate memory, string[] memory) { + require(index < coalitions.length, "Index out of range"); + + Coalition storage coalition = coalitions[index]; + string[] memory partyNames = new string[](coalition.parties.length); + uint256 count = 0; - constructor(uint256 _electionStart, uint256 _electionEnd) Election(_electionStart, _electionEnd) { + for (uint256 i = 0; i < coalition.parties.length; i++) { + string memory partyName = coalition.parties[i]; + partyNames[count] = partyName; + count++; + } + return (coalition.majorCandidate, partyNames); } -} \ No newline at end of file + /// @notice as first step, during the registration period the parties register their names and list of councilior candidates + function registerParty( + string memory name, + string[] memory counciliorCandidates + ) external onlyOwner isRegistrationPeriod { + require( + counciliorCandidates.length == 5, + "The list of councilior candidates must be composed of 5 names" + ); + + Candidate[] storage candidatesList = parties[name]; + + for (uint i = 0; i < counciliorCandidates.length; i++) { + require( + !isCounciliorCandidateInArray[counciliorCandidates[i]], + "Councilior candidate names must be unique" + ); + + Candidate memory c = Candidate({ + name: counciliorCandidates[i], + candidatesFor: "councilior", + points: 0 + }); + + candidatesList.push(c); + isCounciliorCandidateInArray[counciliorCandidates[i]] = true; + } + + partiesLength++; + } + + /// @notice in this second step, the parties registered compose coalitions and indicate a major candidate name. A coalition is composed by one or more parties. + function registerCoalition( + string memory majorCandidate, + string[] memory coalitionParties + ) external onlyOwner isRegistrationPeriod { + require( + _arePartiesRegistered(coalitionParties), + "One or more parties are not registered. Proceed with the registration first" + ); + require( + !_arePartiesAlreadyInCoalition(coalitionParties), + "One or more parties are already present in a registered coalition" + ); + require( + !_isMajorCandidateAlreadyRegistered(majorCandidate), + "The major candidate is already registered with a coalition" + ); + + Candidate memory mcandidate = Candidate({ + name: majorCandidate, + candidatesFor: "major", + points: 0 + }); + + Coalition memory newCoalition; + newCoalition.majorCandidate = mcandidate; + newCoalition.parties = coalitionParties; + + coalitions.push(newCoalition); + } + + function _arePartiesRegistered( + string[] memory partiesToCheck + ) private view returns (bool) { + for (uint i = 0; i < partiesToCheck.length; i++) { + if (parties[partiesToCheck[i]].length != 0) { + return true; + } + } + return false; + } + + function _arePartiesAlreadyInCoalition( + string[] memory partiesToCheck + ) private view returns (bool) { + for (uint i = 0; i < coalitions.length; i++) { + for (uint j = 0; j < coalitions[i].parties.length; j++) { + for (uint k = 0; k < partiesToCheck.length; k++) { + if ( + keccak256(bytes(coalitions[i].parties[j])) == + keccak256(bytes(partiesToCheck[k])) + ) { + return true; + } + } + } + } + return false; + } + + function _isMajorCandidateAlreadyRegistered( + string memory candidateMajor + ) private view returns (bool) { + for (uint i = 0; i < coalitions.length; i++) { + if ( + keccak256( + abi.encodePacked(coalitions[i].majorCandidate.name) + ) == keccak256(abi.encodePacked(candidateMajor)) + ) { + return true; + } + } + return false; + } +} diff --git a/contracts/RegionalElection.sol b/contracts/RegionalElection.sol index 2eff059..cd35bcb 100644 --- a/contracts/RegionalElection.sol +++ b/contracts/RegionalElection.sol @@ -8,7 +8,12 @@ import "./Election.sol"; /// @custom:experimental This is an experimental contract. contract RegionalElection is Election { - constructor(uint256 _electionStart, uint256 _electionEnd) Election(_electionStart, _electionEnd) { + constructor( + string memory _name, + uint256 _registrationStart, + uint256 _registrationEnd, + uint8 _votingPoints + ) Election(_name, _registrationStart, _registrationEnd, _votingPoints) { } diff --git a/docs/assets/agora-main-flow.png b/docs/assets/agora-main-flow.png index d7435a1..b6edf01 100644 Binary files a/docs/assets/agora-main-flow.png and b/docs/assets/agora-main-flow.png differ diff --git a/election-scripts/__mocks__.ts b/election-scripts/__mocks__.ts new file mode 100644 index 0000000..5654604 --- /dev/null +++ b/election-scripts/__mocks__.ts @@ -0,0 +1,62 @@ +import { Party } from "./types"; + +export const MUNICIPALITY_ELECTION_DATA = { + name: "Election of major of Braccagni city", + municipality: "Braccagni", + region: "Toscana", + country: "Italy", + registrationStart: 1712269847, + registrationEnd: 1717256752, + votingPoints: 20, +}; + +export const PARTY_NAME_A = "Partito Democratico"; +export const PARTY_NAME_B = "Forza Italia"; +export const PARTY_NAME_C = "Cinque Stelle"; +export const PARTY_NAME_D = "Lega"; + +export const MAJOR_CANDIDATE_1 = "Pino Pini"; +export const MAJOR_CANDIDATE_2 = "Ugo Silenti"; + +export const PARTIES: Party[] = [ + { + name: PARTY_NAME_A, + councilorCandidates: [ + "Luigi Rossi", + "Maria Verdi", + "Renato Bianchi", + "Francesco Guidi", + "Paolo Franchi", + ], + }, + { + name: PARTY_NAME_B, + councilorCandidates: [ + "Francesca Riti", + "Vanessa Reti", + "Mario Checchi", + "Carlo Proni", + "Pierpaolo Pingitore", + ], + }, + { + name: PARTY_NAME_C, + councilorCandidates: [ + "Giuseppe Toni", + "Nicolò Movizzo", + "Alessandra Tonali", + "Antonella Chierici", + "Antonio Basso", + ], + }, + { + name: PARTY_NAME_D, + councilorCandidates: [ + "Patrizio Pini", + "Mariagrazia Crudi", + "Sabrina Giacigli", + "Marco Lioni", + "Pio Pedri", + ], + }, +]; diff --git a/election-scripts/create-election.test.ts b/election-scripts/create-election.test.ts new file mode 100644 index 0000000..52145af --- /dev/null +++ b/election-scripts/create-election.test.ts @@ -0,0 +1,53 @@ +import { ethers } from "hardhat"; +import { expect } from "chai"; +import { main } from "./create-election"; +import { PARTIES } from "./__mocks__"; + +describe("Create Election Script", () => { + it("should deploy contract and register parties and coalitions", async () => { + const response = await main({ + name: "Election of major of Braccagni city", + municipality: "Braccagni", + region: "Toscana", + country: "Italy", + registrationStart: 0, + registrationEnd: 9999999999, + votingPoints: 20, + }); + + expect(response).to.exist; + expect(response.result).to.equal("ok"); + + const contractAddress = response.data!.contractAddress; + const contract = await ethers.getContractAt( + "MunicipalityElection", + contractAddress, + ); + + for (const party of PARTIES) { + const candidatesRegistered = await contract.getCandidatesByParty( + party.name, + ); + expect(candidatesRegistered).to.deep.equal(party.councilorCandidates); + } + + const coalitions = response.data!.coalitions; + for (const coalition of coalitions) { + const coalitionFromContract = await contract.getCoalition( + coalitions.indexOf(coalition), + ); + expect(coalitionFromContract[0][0]).to.equal( + coalition.majorCandidate.name, + ); + expect(coalitionFromContract[0][1]).to.equal( + coalition.majorCandidate.candidatesFor, + ); + expect(coalitionFromContract[0][2]).to.equal( + coalition.majorCandidate.points, + ); + expect(coalitionFromContract[1]).to.deep.equal( + coalition.parties.map((p) => p.name), + ); + } + }); +}); diff --git a/election-scripts/create-election.ts b/election-scripts/create-election.ts new file mode 100644 index 0000000..b3130f0 --- /dev/null +++ b/election-scripts/create-election.ts @@ -0,0 +1,157 @@ +/** + * In order to run this script in hardhat, run the command: npx hardhat run script/create-election.ts + * to run the script over a network configured in the hardhat.config.ts run: + * npx hardhat run script/create-voter-eoa.ts --network , example: + * npx hardhat run script/create-voter-eoa.ts --network sepolia + * + * This is the third step of the voting process: a public authority creates an election by + * deploying the election smart contract passing the required data in the constructor. + * Then, the script registers the parties and the names of the councilior and major candidates. + */ +import { ethers } from "hardhat"; +import { + Response, + result, + Ballot, + Party, + Coalition, + Candidature, + ElectionData, +} from "./types"; +import { Signer } from "ethers"; +import { MunicipalityElection } from "../typechain-types/MunicipalityElection"; +import { + PARTIES, + MUNICIPALITY_ELECTION_DATA, + MAJOR_CANDIDATE_1, + MAJOR_CANDIDATE_2, + PARTY_NAME_A, + PARTY_NAME_B, + PARTY_NAME_C, + PARTY_NAME_D, +} from "./__mocks__"; + +let owner: Signer; +/** + * This function deploys the contract, registers the parties, the coalitions, the councilor candidates and the + * major candidate to a given municipality election contract and returns the list of the data registered ready to be use for a ballot. + * + * @param {ElectionData} electionData - data required to deploy the smart contract, used for testing purposes + * @returns {Promise>} - this response contains the data of the ballot to be used for voting + */ +export async function main( + electionData?: ElectionData, +): Promise> { + const response: Response = { + result: result.OK, + }; + + const ContractFactory = await ethers.getContractFactory( + "MunicipalityElection", + ); + + [owner] = await ethers.getSigners(); + + const contract: MunicipalityElection = await ContractFactory.deploy( + electionData?.name || MUNICIPALITY_ELECTION_DATA.name, + electionData?.municipality || MUNICIPALITY_ELECTION_DATA.municipality, + electionData?.region || MUNICIPALITY_ELECTION_DATA.region, + electionData?.country || MUNICIPALITY_ELECTION_DATA.country, + electionData?.registrationStart || + MUNICIPALITY_ELECTION_DATA.registrationStart, + electionData?.registrationEnd || MUNICIPALITY_ELECTION_DATA.registrationEnd, + electionData?.votingPoints || MUNICIPALITY_ELECTION_DATA.votingPoints, + ); + + const address = await contract.getAddress(); + const parties: Party[] = []; + const coalitions: Coalition[] = []; + + for (const party of PARTIES) { + await contract + .connect(owner) + .registerParty(party.name, party.councilorCandidates); + const candidatesRegistered = await contract.getCandidatesByParty( + party.name, + ); + parties.push(party); + + console.log( + `registered party ${party.name} with candidates: ${JSON.stringify(candidatesRegistered)}`, + ); + } + + await contract + .connect(owner) + .registerCoalition(MAJOR_CANDIDATE_1, [PARTY_NAME_A, PARTY_NAME_B]); + const coalition1Raw = await contract.getCoalition(0); + + const coalition1: Coalition = { + majorCandidate: { + name: coalition1Raw[0][0], + candidatesFor: coalition1Raw[0][1] as Candidature, + points: Number(coalition1Raw[0][2]), + }, + parties: [], + }; + + for (const p of parties) { + for (const k of coalition1Raw[1]) { + if (k === p.name) { + coalition1.parties.push(p); + } + } + } + + coalitions.push(coalition1); + + console.log( + `registered coalition for major candidate: ${coalition1Raw[0]} and parties ${coalition1Raw[1]}`, + ); + + await contract + .connect(owner) + .registerCoalition(MAJOR_CANDIDATE_2, [PARTY_NAME_C, PARTY_NAME_D]); + const coalition2Raw = await contract.getCoalition(1); + + const coalition2: Coalition = { + majorCandidate: { + name: coalition2Raw[0][0], + candidatesFor: coalition2Raw[0][1] as Candidature, + points: Number(coalition2Raw[0][2]), + }, + parties: [], + }; + + for (const p of parties) { + for (const k of coalition2Raw[1]) { + if (k === p.name) { + coalition2.parties.push(p); + } + } + } + + coalitions.push(coalition2); + + console.log( + `registered coalition for major candidate: ${coalition2Raw[0]} and parties ${coalition2Raw[1]}`, + ); + + const ballot: Ballot = { + contractAddress: address, + coalitions, + }; + + response.data = ballot; + + return response; +} + +main() + .then((response) => { + console.log(JSON.stringify(response)); + return response; + }) + .catch((error) => { + console.error(error); + }); diff --git a/election-scripts/create-voter-eoa.ts b/election-scripts/create-voter-eoa.ts index 16ca946..c179fed 100644 --- a/election-scripts/create-voter-eoa.ts +++ b/election-scripts/create-voter-eoa.ts @@ -2,7 +2,7 @@ * In order to run this script in hardhat, run the command: npx hardhat run script/create-voter-eoa.ts * to run the script over a network configured in the hardhat.config.ts run: * npx hardhat run script/create-voter-eoa.ts --network , example: - * npx hardhat run script/create-voter-eoa.ts --network goerli + * npx hardhat run script/create-voter-eoa.ts --network sepolia * * This is the first step of the voting process: a public authority creates an EOA for the Voter, the * script returns the public address and the private key that are communicated to the Voter. @@ -10,6 +10,10 @@ import { ethers } from "hardhat"; import { Response, EOAResponse, result } from "./types"; +/** + * + * @returns {Promise>} - This response contains the address/privateKey pair of the EOA created + */ export async function main(): Promise> { const response: Response = { result: result.OK, @@ -35,6 +39,7 @@ export async function main(): Promise> { main() .then((response) => { console.log(response); + return response; }) .catch((error) => { console.error(error); diff --git a/election-scripts/types.ts b/election-scripts/types.ts index d99a19d..4c7757b 100644 --- a/election-scripts/types.ts +++ b/election-scripts/types.ts @@ -13,3 +13,52 @@ export type EOAResponse = { address: string; privateKey: string; }; + +export enum Candidature { + MAJOR = "major", + COUNCILOR = "councilor", +} + +export type Candidate = { + name: string; + candidatesFor: Candidature; + points: number; +}; + +export type Party = { + name: string; + councilorCandidates: string[]; +}; + +export type Coalition = { + majorCandidate: Candidate; + parties: Party[]; +}; + +export type MunicipalityElection = { + country: string; + region: string; + municipality: string; + votingPoints: number; + coalitions: Coalition[]; + registrationStartDate: number; + registrationEndDate: number; + electionStartDate: number; + electionEndDate: number; + status: string; +}; + +export type Ballot = { + contractAddress: string; + coalitions: Coalition[]; +}; + +export type ElectionData = { + name: string; + municipality: string; + region: string; + country: string; + registrationStart: number; + registrationEnd: number; + votingPoints: number; +}; diff --git a/hardhat.config.ts b/hardhat.config.ts index beedefd..6c9e73b 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -29,7 +29,7 @@ const config: HardhatUserConfig = { // Version of the EVM to compile for. // Affects type checking and code generation. Can be homestead, // tangerineWhistle, spuriousDragon, byzantium, constantinople, petersburg, istanbul or berlin - evmVersion: "byzantium", + evmVersion: "constantinople", debug: { // How to treat revert (and require) reason strings. Settings are // "default", "strip", "debug" and "verboseDebug". @@ -50,6 +50,9 @@ const config: HardhatUserConfig = { url: SEPOLIA_URL, accounts: [ALCHEMY_PRIVATE_KEY], }, + localhost: { + url: "http://127.0.0.1:8545/", + }, }, }; diff --git a/ignition/modules/CountryElection.ts b/ignition/modules/CountryElection.ts new file mode 100644 index 0000000..76ce720 --- /dev/null +++ b/ignition/modules/CountryElection.ts @@ -0,0 +1,10 @@ +import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; + +export default buildModule("CountryElection", (m) => { + const countryElection = m.contract("CountryElection", [ + m.getParameter("registrationStart"), + m.getParameter("registrationEnd"), + ]); + + return { countryElection }; +}); diff --git a/ignition/modules/MunicipalityElection.ts b/ignition/modules/MunicipalityElection.ts new file mode 100644 index 0000000..fcfb185 --- /dev/null +++ b/ignition/modules/MunicipalityElection.ts @@ -0,0 +1,15 @@ +import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; + +export default buildModule("MunicipalityElection", (m) => { + const municipalityElection = m.contract("MunicipalityElection", [ + m.getParameter("name"), + m.getParameter("municipality"), + m.getParameter("region"), + m.getParameter("country"), + m.getParameter("registrationStart"), + m.getParameter("registrationEnd"), + m.getParameter("votingPoints"), + ]); + + return { municipalityElection }; +}); diff --git a/ignition/modules/Registries.ts b/ignition/modules/Registry.ts similarity index 100% rename from ignition/modules/Registries.ts rename to ignition/modules/Registry.ts diff --git a/ignition/parameters.json b/ignition/parameters.json new file mode 100644 index 0000000..33459ba --- /dev/null +++ b/ignition/parameters.json @@ -0,0 +1,15 @@ +{ + "CountryElection": { + "registrationStart": 1714578352000, + "registrationEnd": 1717256752000 + }, + "MunicipalityElection": { + "name": "Election of major of Braccagni city", + "municipality": "Braccagni", + "region": "Toscana", + "country": "Italy", + "registrationStart": 1712269847, + "registrationEnd": 1717256752, + "votingPoints": 20 + } +} \ No newline at end of file diff --git a/package.json b/package.json index bec8fa4..44717c2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "agora", - "version": "0.2.0", + "version": "0.3.0", "description": "A confidentiality-first electronic voting system", "author": { "name": "nova collective", diff --git a/script/commit b/script/commit index 6180b2f..b084302 100755 --- a/script/commit +++ b/script/commit @@ -1,6 +1,8 @@ #!/usr/bin/env sh -npm run duplicated -npm run test-contracts -npm run test-scripts +set -e + +npm run duplicated || exit 1 +npm run test-contracts || exit 1 +npm run test-scripts || exit 1 npx git-cz \ No newline at end of file diff --git a/script/deploy b/script/deploy index ea7214c..cb21199 100755 --- a/script/deploy +++ b/script/deploy @@ -5,7 +5,7 @@ if [ "$#" -ne 2 ]; then exit 1 fi -MODULE_PATH="$1" +MODULE_NAME="$1" NETWORK="$2" -npx hardhat ignition deploy "$MODULE_PATH" --network "$NETWORK" \ No newline at end of file +npx hardhat ignition deploy ignition/modules/"$MODULE_NAME".ts --network "$NETWORK" --parameters ignition/parameters.json \ No newline at end of file diff --git a/test/Election.ts b/test/Election.ts index 8060113..2c40aca 100644 --- a/test/Election.ts +++ b/test/Election.ts @@ -6,6 +6,7 @@ import { Election } from "../typechain-types/Election"; describe("Election", function () { let owner: Signer; let electionContract: Election; + const name = "Mock election"; const now = Date.now(); const tomorrow = now + 86400000; const dayAfterTomorrow = now + 86400000 + 86400000; @@ -13,55 +14,82 @@ describe("Election", function () { beforeEach(async function () { const ElectionFactory = await ethers.getContractFactory("Election", owner); [owner] = await ethers.getSigners(); - electionContract = await ElectionFactory.deploy(tomorrow, dayAfterTomorrow); + electionContract = await ElectionFactory.deploy( + name, + tomorrow, + dayAfterTomorrow, + 20, + ); }); it("should deploy with correct initial values", async function () { const ownerAddress = await electionContract.owner(); expect(ownerAddress).to.equal(await owner.getAddress()); - const start = await electionContract.getElectionStart(); + const start = await electionContract.getRegistrationStart(); expect(start).to.equal(tomorrow); - const end = await electionContract.getElectionEnd(); + const end = await electionContract.getRegistrationEnd(); expect(end).to.equal(dayAfterTomorrow); }); - it("should allow owner to change election start date before election starts", async function () { + it("should allow owner to change registration start date before election starts", async function () { const newStartDate = now + 43200000; - await electionContract.setElectionStart(newStartDate); - const start = await electionContract.getElectionStart(); + await electionContract.setRegistrationStart(newStartDate); + const start = await electionContract.getRegistrationStart(); expect(start).to.equal(newStartDate); }); - it("should allow owner to change election end date before election starts", async function () { + it("should allow owner to change registration end date before election starts", async function () { const newEndDate = dayAfterTomorrow + 86400000; - await electionContract.setElectionEnd(newEndDate); + await electionContract.setRegistrationEnd(newEndDate); - const end = await electionContract.getElectionEnd(); + const end = await electionContract.getRegistrationEnd(); expect(end).to.equal(newEndDate); }); + it("should revert when setting elections start before the end of the registration period", async function () { + const electionStartDate = dayAfterTomorrow + 17280000; + + await expect( + electionContract.setElectionStart(electionStartDate), + ).to.be.revertedWith( + "Elections can't start before the end of the registration process", + ); + }); + it("should not allow voting before election period", async function () { await expect(electionContract.vote()).to.be.revertedWith( "Elections are not open", ); }); - it("should not allow owner to change election start date during election or after it ends", async function () { + it("should not allow owner to change registration start date during registration period or after it ends", async function () { const now = Math.floor(Date.now() / 1000); - await electionContract.setElectionStart(now); + await electionContract.setRegistrationStart(now); - await expect(electionContract.setElectionStart(now + 1)).to.be.revertedWith( - "Elections have already started, it's too late for changing the start of the elections", + await expect( + electionContract.setRegistrationStart(now + 1), + ).to.be.revertedWith( + "Registrations have already started, it's too late for changing the start of the registration", ); await ethers.provider.send("evm_increaseTime", [tomorrow]); - await expect(electionContract.setElectionStart(300)).to.be.revertedWith( - "Elections are closed, it's not possible to change the start of the elections", + await expect(electionContract.setRegistrationStart(300)).to.be.revertedWith( + "Registration are closed, it's not possible to change the start of the registration", ); }); + + // it("should correctly set and get the elections end", async function () {}); + + // it("should not set the elections start before the end of the registration", async function () {}); + + // it("should not set the elections start during the elections", async function () {}); + + // it("should not set the elections start after the elections closure", async function () {}); + + // it("should not set the elections end after the elections closure", async function () {}); }); diff --git a/test/MunicipalityElection.ts b/test/MunicipalityElection.ts new file mode 100644 index 0000000..ff51724 --- /dev/null +++ b/test/MunicipalityElection.ts @@ -0,0 +1,94 @@ +import { ethers } from "hardhat"; +import { expect } from "chai"; +import { Signer } from "ethers"; +import { MunicipalityElection } from "../typechain-types/MunicipalityElection"; + +describe("MunicipalityElection Contract", function () { + let contract: MunicipalityElection; + let owner: Signer; + + const electionName = "Mock Election"; + const partyName = "Party A"; + const partyNameB = "PArty B"; + const councilorCandidates = [ + "Candidate 1", + "Candidate 2", + "Candidate 3", + "Candidate 4", + "Candidate 5", + ]; + const councilorCandidatesB = [ + "Candidate 6", + "Candidate 7", + "Candidate 8", + "Candidate 9", + "Candidate 0", + ]; + const majorCandidate = "Major Candidate"; + const coalitionParties = [partyName, partyNameB]; + + beforeEach(async () => { + const ContractFactory = await ethers.getContractFactory( + "MunicipalityElection", + ); + [owner] = await ethers.getSigners(); + + // Deploy the contract with a registration period that includes the current timestamp + contract = (await ContractFactory.deploy( + electionName, + "Municipality", + "Region", + "Country", + 0, + 999999999999999, + 20, + )) as MunicipalityElection; + }); + + it("Should deploy the contract", async function () { + expect(contract.address).to.not.equal(0); + }); + + it("should register a party", async function () { + await contract.connect(owner).registerParty(partyName, councilorCandidates); + const result = await contract.getCandidatesByParty(partyName); + + expect(result.length).to.be.equal(5); + expect(result[0]).to.be.equal("Candidate 1"); + }); + + it("Should register a coalition", async function () { + await contract.connect(owner).registerParty(partyName, councilorCandidates); + await contract + .connect(owner) + .registerParty(partyNameB, councilorCandidatesB); + await contract + .connect(owner) + .registerCoalition(majorCandidate, coalitionParties); + const coalition = await contract.getCoalition(0); + + expect(coalition[0][0]).to.be.equal(majorCandidate); + expect(coalition[0][1]).to.be.equal("major"); + expect(coalition[0][2]).to.be.equal(0); + expect(coalition[1][0]).to.be.equal(partyName); + expect(coalition[1][1]).to.be.equal(partyNameB); + }); + + it("Should prevent to register multiple coalitions with same parties", async function () { + await contract.connect(owner).registerParty(partyName, councilorCandidates); + await contract + .connect(owner) + .registerParty(partyNameB, councilorCandidatesB); + + await contract + .connect(owner) + .registerCoalition(majorCandidate, coalitionParties); + await expect( + contract + .connect(owner) + .registerCoalition(majorCandidate, coalitionParties), + ).to.be.revertedWith( + "One or more parties are already present in a registered coalition", + ); + }); +});