diff --git a/contracts/DEC.sol b/contracts/DEC.sol new file mode 100644 index 0000000..c8f0ee7 --- /dev/null +++ b/contracts/DEC.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.24; + +/// @title The Voter's Digital Electoral Cards +/// @author Christian Palazzo +/// @custom:experimental This is an experimental contract. +contract DEC { + + address public owner; + + constructor() { + /// @dev only the owner of the contract has write permissions + owner = msg.sender; + } + + modifier onlyOwner() { + require(msg.sender == owner, "Only owner can call this function"); + _; + } + + + /// @notice This is the Digital Electoral Card, emitted by a public third-party authority and owned by the Voter + /// @dev This data is encrypted with the Voter's public address and only the Voter can decrypt it using the private key + struct decData { + string taxCode; + string municipality; + string province; + string region; + string country; + } + + event DECEncrypted(address indexed owner, bytes encryptedData); + + + /// @notice This function is used to encrypt ad digitally sign a DEC + function encryptDEC(decData memory dec) public onlyOwner returns (bytes memory) { + bytes memory encodedData = abi.encodePacked( + dec.taxCode, dec.municipality, dec.province, dec.region, dec.country + ); + bytes32 hashedData = keccak256(encodedData); + bytes memory signature = signData(hashedData); + + emit DECEncrypted(msg.sender, abi.encodePacked(hashedData, signature)); + + return abi.encodePacked(hashedData, signature); + } + + + /// @notice This function is used to digitally sign the data + function signData(bytes32 data) private pure returns (bytes memory) { + bytes32 hash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", data)); + bytes1 v = bytes1(0); + bytes32 r = bytes32(0); + bytes32 s = uintToBytes32(1); + return abi.encodePacked(ecrecover(hash, uint8(v), r, s), r, s); + } + + /// @notice this function is used in signData function + function uintToBytes32(uint256 x) private pure returns (bytes32) { + return bytes32(x); + } +} \ No newline at end of file diff --git a/contracts/DECsRegistry.sol b/contracts/DECsRegistry.sol new file mode 100644 index 0000000..64a2dcc --- /dev/null +++ b/contracts/DECsRegistry.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.24; + +import "./DEC.sol"; + +/// @title The Registry of the Digital Electoral Cards +/// @author Christian Palazzo +/// @custom:experimental This is an experimental contract. +contract DECsRegistry is DEC { + + constructor() DEC() { + + } + + /// @notice this is the list of stamps of elections in which the voter participated + /// @dev the first address is related to the Voter's EOA, the second array is the Voter's stamps list + mapping (address => address[]) electoralStamps; + + /// @notice this function contains the list of DECs + /// @dev the address is related to the Voter's EOA + mapping (address => bytes) registry; + + event DECRegistered(address indexed voter, bytes dec); + event DECStamped(address indexed election, address indexed voter); + + + /// @notice this function is used by the third party authority to register a Voter's DEC in the registry + /// @dev the DEC contains sensitive data that must be encrypted + function registerDEC(decData memory dec, address voter) public onlyOwner { + require(registry[voter].length == 0, "The Voter's DEC has been already registered"); + registry[voter] = encryptDEC(dec); + emit DECRegistered(voter, registry[voter]); + return; + } + + + /// @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(bytes memory) { + require(registry[voter].length != 0, "The Voter don't have a registered DEC"); + return registry[voter]; + } + + + /// @notice this function checks in the registry if the Voter already voted in a certail election + function hasVoterAlreadyVoted(address voter, address election) public view returns (bool) { + for (uint i = 0; i < electoralStamps[voter].length; i++) { + if (electoralStamps[voter][i] == election) { + return true; + } + } + return false; + } + + /// @notice this function put the election stamp on the Voter's DEC after the vote + /// @dev the owner of the DECs registry is the same of the election smart contract (third party authority) + function stampsTheDEC(address election, address voter) public onlyOwner { + electoralStamps[voter].push(election); + emit DECStamped(election, voter); + return; + } +} \ No newline at end of file diff --git a/contracts/Lock.sol b/contracts/Lock.sol deleted file mode 100644 index 1efbef3..0000000 --- a/contracts/Lock.sol +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.24; - -// Uncomment this line to use console.log -// import "hardhat/console.sol"; - -contract Lock { - uint public unlockTime; - address payable public owner; - - event Withdrawal(uint amount, uint when); - - constructor(uint _unlockTime) payable { - require( - block.timestamp < _unlockTime, - "Unlock time should be in the future" - ); - - unlockTime = _unlockTime; - owner = payable(msg.sender); - } - - function withdraw() public { - // Uncomment this line, and the import of "hardhat/console.sol", to print a log in your terminal - // console.log("Unlock time is %o and block timestamp is %o", unlockTime, block.timestamp); - - require(block.timestamp >= unlockTime, "You can't withdraw yet"); - require(msg.sender == owner, "You aren't the owner"); - - emit Withdrawal(address(this).balance, block.timestamp); - - owner.transfer(address(this).balance); - } -} diff --git a/docs/assets/agora_main_sequence_diagrams-Phase 0.jpg b/docs/assets/agora_main_sequence_diagrams-Phase 0.jpg index abc111b..f91ab44 100644 Binary files a/docs/assets/agora_main_sequence_diagrams-Phase 0.jpg and b/docs/assets/agora_main_sequence_diagrams-Phase 0.jpg differ diff --git a/docs/assets/agora_main_sequence_diagrams-Phase 2.jpg b/docs/assets/agora_main_sequence_diagrams-Phase 2.jpg index 22fb9d7..8b38d3a 100644 Binary files a/docs/assets/agora_main_sequence_diagrams-Phase 2.jpg and b/docs/assets/agora_main_sequence_diagrams-Phase 2.jpg differ diff --git a/docs/assets/agora_main_sequence_diagrams-Phase 3.jpg b/docs/assets/agora_main_sequence_diagrams-Phase 3.jpg index 420ba95..3c06ea2 100644 Binary files a/docs/assets/agora_main_sequence_diagrams-Phase 3.jpg and b/docs/assets/agora_main_sequence_diagrams-Phase 3.jpg differ diff --git a/docs/assets/agora_main_sequence_diagrams-Phase 4.jpg b/docs/assets/agora_main_sequence_diagrams-Phase 4.jpg index 060d646..88036ce 100644 Binary files a/docs/assets/agora_main_sequence_diagrams-Phase 4.jpg and b/docs/assets/agora_main_sequence_diagrams-Phase 4.jpg differ diff --git a/docs/assets/decs_registry_class.png b/docs/assets/decs_registry_class.png new file mode 100644 index 0000000..03964de Binary files /dev/null and b/docs/assets/decs_registry_class.png differ diff --git a/docs/assets/election_class.png b/docs/assets/election_class.png new file mode 100644 index 0000000..c5e47e4 Binary files /dev/null and b/docs/assets/election_class.png differ diff --git a/docs/assets/electoral_card_class.png b/docs/assets/electoral_card_class.png index 0bfacc9..7ac5c3c 100644 Binary files a/docs/assets/electoral_card_class.png and b/docs/assets/electoral_card_class.png differ diff --git a/docs/assets/registration_class.png b/docs/assets/registration_class.png deleted file mode 100644 index 994d5b3..0000000 Binary files a/docs/assets/registration_class.png and /dev/null differ diff --git a/docs/assets/voting_class.png b/docs/assets/voting_class.png deleted file mode 100644 index cd80e46..0000000 Binary files a/docs/assets/voting_class.png and /dev/null differ diff --git a/script/create-voter-eoa.test.ts b/script/create-voter-eoa.test.ts index 24e7e37..aa69390 100644 --- a/script/create-voter-eoa.test.ts +++ b/script/create-voter-eoa.test.ts @@ -3,13 +3,6 @@ import { ethers } from "hardhat"; import { main } from "./create-voter-eoa"; describe("Main function", () => { - /*beforeEach(() => { - ethers.Wallet.createRandom = jest.fn().mockReturnValueOnce({ - address: "mockedAddress", - privateKey: "mockedPrivateKey", - }); - });*/ - afterEach(() => { jest.clearAllMocks(); }); diff --git a/test/DEC.ts b/test/DEC.ts new file mode 100644 index 0000000..a543c45 --- /dev/null +++ b/test/DEC.ts @@ -0,0 +1,35 @@ +import { ethers } from "hardhat"; +import { expect } from "chai"; + +describe("DEC Contract", function () { + let DEC: any; + let decContract: any; + let ownerAddress: any; + + before(async function () { + DEC = await ethers.getContractFactory("DEC"); + [ownerAddress] = await ethers.getSigners(); + }); + + beforeEach(async function () { + decContract = await DEC.deploy(); + }); + + it("should deploy the contract and set the owner", async function () { + expect(await decContract.owner()).to.equal(ownerAddress.address); + }); + + it("should encrypt DEC data correctly", async function () { + const decData = { + taxCode: "123456789", + municipality: "Sample Municipality", + province: "Sample Province", + region: "Sample Region", + country: "Sample Country", + }; + + const encryptedData = await decContract.encryptDEC(decData); + + expect(encryptedData.data).to.not.be.null; + }); +}); diff --git a/test/DECsRegistry.ts b/test/DECsRegistry.ts new file mode 100644 index 0000000..6bebe2c --- /dev/null +++ b/test/DECsRegistry.ts @@ -0,0 +1,84 @@ +import { ethers } from "hardhat"; +import { expect } from "chai"; +import { Signer } from "ethers"; +import { DECsRegistry } from "../typechain-types/DECsRegistry"; +import { DecData } from "./types"; + +describe("DECs Registry Contract", function () { + let contract: DECsRegistry; + let owner: Signer; + let voter: Signer; + + const electionAddress = "0xae92d5aD7583AD66E49A0c67BAd18F6ba52dDDc1"; + const decData: DecData = { + taxCode: "1234567890", + municipality: "mockMunicipality", + province: "mockProvince", + region: "mockRegion", + country: "mockCountry", + }; + + beforeEach(async () => { + const ContractFactory = await ethers.getContractFactory("DECsRegistry"); + [owner, voter] = await ethers.getSigners(); + contract = await ContractFactory.deploy(); + }); + + it("Should deploy the contract", async function () { + expect(contract.address).to.not.equal(0); + }); + + it("Should register DEC", async function () { + const response = await contract + .connect(owner) + .registerDEC(decData, await voter.getAddress()); + + expect(response.blockHash).to.not.equal(null); + expect(response.blockHash).to.not.equal(undefined); + expect(response.data.length).to.be.greaterThan(0); + }); + + it("Should not register DEC if already registered", async function () { + await contract + .connect(owner) + .registerDEC(decData, await voter.getAddress()); + await expect( + contract.connect(owner).registerDEC(decData, await voter.getAddress()), + ).to.be.revertedWith("The Voter's DEC has been already registered"); + }); + + it("Should get DEC", async function () { + await contract + .connect(owner) + .registerDEC(decData, await voter.getAddress()); + const retrievedDEC = await contract.getDEC(await voter.getAddress()); + + expect(retrievedDEC.length).to.be.greaterThan(0); + }); + + it("Should revert if DEC not registered", async function () { + await expect(contract.getDEC(await voter.getAddress())).to.be.revertedWith( + "The Voter don't have a registered DEC", + ); + }); + + it("Should return true if voter already voted", async function () { + await contract + .connect(owner) + .stampsTheDEC(electionAddress, await voter.getAddress()); + + const hasVoted = await contract.hasVoterAlreadyVoted( + await voter.getAddress(), + electionAddress, + ); + expect(hasVoted).to.be.true; + }); + + it("Should return false if voter hasn't voted", async function () { + const hasVoted = await contract.hasVoterAlreadyVoted( + await voter.getAddress(), + electionAddress, + ); + expect(hasVoted).to.be.false; + }); +}); diff --git a/test/Lock.ts b/test/Lock.ts deleted file mode 100644 index 160dbfa..0000000 --- a/test/Lock.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { - time, - loadFixture, -} from "@nomicfoundation/hardhat-toolbox/network-helpers"; -import { anyValue } from "@nomicfoundation/hardhat-chai-matchers/withArgs"; -import { expect } from "chai"; -import hre from "hardhat"; - -describe("Lock", function () { - // We define a fixture to reuse the same setup in every test. - // We use loadFixture to run this setup once, snapshot that state, - // and reset Hardhat Network to that snapshot in every test. - async function deployOneYearLockFixture() { - const ONE_YEAR_IN_SECS = 365 * 24 * 60 * 60; - const ONE_GWEI = 1_000_000_000; - - const lockedAmount = ONE_GWEI; - const unlockTime = (await time.latest()) + ONE_YEAR_IN_SECS; - - // Contracts are deployed using the first signer/account by default - const [owner, otherAccount] = await hre.ethers.getSigners(); - - const Lock = await hre.ethers.getContractFactory("Lock"); - const lock = await Lock.deploy(unlockTime, { value: lockedAmount }); - - return { lock, unlockTime, lockedAmount, owner, otherAccount }; - } - - describe("Deployment", function () { - it("Should set the right unlockTime", async function () { - const { lock, unlockTime } = await loadFixture(deployOneYearLockFixture); - - expect(await lock.unlockTime()).to.equal(unlockTime); - }); - - it("Should set the right owner", async function () { - const { lock, owner } = await loadFixture(deployOneYearLockFixture); - - expect(await lock.owner()).to.equal(owner.address); - }); - - it("Should receive and store the funds to lock", async function () { - const { lock, lockedAmount } = await loadFixture( - deployOneYearLockFixture - ); - - expect(await hre.ethers.provider.getBalance(lock.target)).to.equal( - lockedAmount - ); - }); - - it("Should fail if the unlockTime is not in the future", async function () { - // We don't use the fixture here because we want a different deployment - const latestTime = await time.latest(); - const Lock = await hre.ethers.getContractFactory("Lock"); - await expect(Lock.deploy(latestTime, { value: 1 })).to.be.revertedWith( - "Unlock time should be in the future" - ); - }); - }); - - describe("Withdrawals", function () { - describe("Validations", function () { - it("Should revert with the right error if called too soon", async function () { - const { lock } = await loadFixture(deployOneYearLockFixture); - - await expect(lock.withdraw()).to.be.revertedWith( - "You can't withdraw yet" - ); - }); - - it("Should revert with the right error if called from another account", async function () { - const { lock, unlockTime, otherAccount } = await loadFixture( - deployOneYearLockFixture - ); - - // We can increase the time in Hardhat Network - await time.increaseTo(unlockTime); - - // We use lock.connect() to send a transaction from another account - await expect(lock.connect(otherAccount).withdraw()).to.be.revertedWith( - "You aren't the owner" - ); - }); - - it("Shouldn't fail if the unlockTime has arrived and the owner calls it", async function () { - const { lock, unlockTime } = await loadFixture( - deployOneYearLockFixture - ); - - // Transactions are sent using the first signer by default - await time.increaseTo(unlockTime); - - await expect(lock.withdraw()).not.to.be.reverted; - }); - }); - - describe("Events", function () { - it("Should emit an event on withdrawals", async function () { - const { lock, unlockTime, lockedAmount } = await loadFixture( - deployOneYearLockFixture - ); - - await time.increaseTo(unlockTime); - - await expect(lock.withdraw()) - .to.emit(lock, "Withdrawal") - .withArgs(lockedAmount, anyValue); // We accept any value as `when` arg - }); - }); - - describe("Transfers", function () { - it("Should transfer the funds to the owner", async function () { - const { lock, unlockTime, lockedAmount, owner } = await loadFixture( - deployOneYearLockFixture - ); - - await time.increaseTo(unlockTime); - - await expect(lock.withdraw()).to.changeEtherBalances( - [owner, lock], - [lockedAmount, -lockedAmount] - ); - }); - }); - }); -}); diff --git a/test/types.ts b/test/types.ts new file mode 100644 index 0000000..4b561a1 --- /dev/null +++ b/test/types.ts @@ -0,0 +1,7 @@ +export interface DecData { + taxCode: string; + municipality: string; + province: string; + region: string; + country: string; +}