From ed96d8daa80a7903a42e08a1700bbf2a90f5f0ce Mon Sep 17 00:00:00 2001 From: Christian Palazzo Date: Sun, 31 Mar 2024 15:32:57 +0200 Subject: [PATCH] Ag 25 deploy contracts (#13) * feat(@agora): AG-25 configured evm compiler configured the evm compiler in the hardhat configuration file * refactor(@contracts): AG-25 refactored DECs Registry contract refactored DECs registry contract * refactor(@contracts): AG-25 refactored DEC contract refactored DEC smart contract * feat(@contracts): AG-25 added name property to DECs Registry added name property to DECs Registry smart contract * feat(@contracts): ignition modules implementation implemented the ignition modules for contracts deploy * fix(@contracts): AG-25 added folder to gitignore added foldet to gitignore * fix(@contracts): AG-25 ignored files added files to gitignore --- .gitignore | 5 +- README.md | 27 +++++++ contracts/DEC.sol | 76 +++++++++---------- contracts/DECsRegistry.sol | 50 ++++++++---- .../create-voter-eoa.test.ts | 0 .../create-voter-eoa.ts | 0 {script => election-scripts}/types.ts | 0 hardhat.config.ts | 32 +++++++- ignition/modules/DECs.ts | 14 ++++ ignition/modules/Registries.ts | 9 +++ package-lock.json | 6 +- package.json | 10 ++- script/deploy | 11 +++ test/DEC.ts | 72 ++++++++++++------ test/DECsRegistry.ts | 33 ++++---- test/types.ts | 7 -- test/utils.ts | 11 +++ 17 files changed, 258 insertions(+), 105 deletions(-) rename {script => election-scripts}/create-voter-eoa.test.ts (100%) rename {script => election-scripts}/create-voter-eoa.ts (100%) rename {script => election-scripts}/types.ts (100%) create mode 100644 ignition/modules/DECs.ts create mode 100644 ignition/modules/Registries.ts create mode 100755 script/deploy delete mode 100644 test/types.ts create mode 100644 test/utils.ts diff --git a/.gitignore b/.gitignore index 540a882..16324e9 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,7 @@ node_modules /coverage.json # reports folder -report \ No newline at end of file +report + +#ignition files +/ignition/deployments \ No newline at end of file diff --git a/README.md b/README.md index 451b44f..feda28f 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ To setup the application follow these steps: * `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; * `REPORT_GAS` enable or disable the gas report on smart contracts unit tests executions; + * `NODE_ENV` set `development` for your local machine; ## How to commit @@ -51,6 +52,32 @@ Smart contracts code coverage documentation [here](https://www.npmjs.com/package CI/CD workflow fails if the unit test code coverage threshold (**80% of lines of code**) for scripts is not met. +## Run the localhost development network + +Hardhat framework provides a local blockchain network that lives in memory, that is useful to test local developments. +To start the network run the command: + +`npm run node:start` + +## Compile the smart contracts + +Run the command: `npm run compile` + +## Deploy the smart contract + +The smart contracts deploy process is managed, under the hood, by [Ignition](https://hardhat.org/ignition/docs/getting-started#overview). + +To deploy smart contract instances run the command: + +`npm run deploy-contract ` + +Ignition will deploy the instances of the smart contract following the logic specified in the ignition module. + +To deploy to a specific network (e.g. mainnet, sepora), the network must be configured in the `hardhat.config.ts` file. + +For the local network the parameter to pass is `localhost`, there is no need to configure the local network. + + # Donations Support this project and offer me a crypto-coffee!! diff --git a/contracts/DEC.sol b/contracts/DEC.sol index a8b48c8..d18b581 100644 --- a/contracts/DEC.sol +++ b/contracts/DEC.sol @@ -1,15 +1,28 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.24; -/// @title The Voter's Digital Electoral Cards +/// @title The Voter's Digital Electoral Card /// @author Christian Palazzo /// @custom:experimental This is an experimental contract. contract DEC { address public owner; + bytes taxCode; + bytes municipality; + bytes region; + bytes country; - constructor() { + constructor( + bytes memory _taxCode, + bytes memory _municipality, + bytes memory _region, + bytes memory _country + ) { /// @dev only the owner of the contract has write permissions owner = msg.sender; + taxCode = _taxCode; + municipality = _municipality; + region = _region; + country = _country; } modifier onlyOwner() { @@ -17,50 +30,37 @@ contract DEC { _; } - /// @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; + function setTaxCode(bytes memory _taxCode) public onlyOwner { + taxCode = _taxCode; } - event DECEncrypted(address indexed owner, bytes encryptedData); + function getTaxCode() public view returns (bytes memory) { + return taxCode; + } - /// @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); + function setMunicipality(bytes memory _municipality) public onlyOwner { + municipality = _municipality; + } - emit DECEncrypted(msg.sender, abi.encodePacked(hashedData, signature)); + function getMunicipality() public view returns (bytes memory) { + return municipality; + } - return abi.encodePacked(hashedData, signature); + function setRegion(bytes memory _region) public onlyOwner { + region = _region; } - /// @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); + function getRegion() public view returns (bytes memory) { + return region; } - /// @notice this function is used in signData function - function uintToBytes32(uint256 x) private pure returns (bytes32) { - return bytes32(x); + function setCountry(bytes memory _country) public onlyOwner { + country = _country; } + + function getCountry() public view returns (bytes memory) { + return country; + } + + } diff --git a/contracts/DECsRegistry.sol b/contracts/DECsRegistry.sol index cf4f7c2..b10f24d 100644 --- a/contracts/DECsRegistry.sol +++ b/contracts/DECsRegistry.sol @@ -6,36 +6,57 @@ 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() {} +contract DECsRegistry { + address public owner; + string public name; /// @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 + /// @dev the first address is related to the Voter's DEC, 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; + /// @dev the first address is related to the Voter's EOA, the second address is related to the DEC + mapping(address => address) registry; - event DECRegistered(address indexed voter, bytes dec); + event DECRegistered(address indexed voter, address dec); event DECStamped(address indexed election, address indexed voter); + + constructor(string memory _name) { + /// @dev only the owner of the contract has write permissions + owner = msg.sender; + name = _name; + } + + modifier onlyOwner() { + require(msg.sender == owner, "Only owner can call this function"); + _; + } + + /// @notice DECs REgistry name setter function + function setName(string memory _name) public onlyOwner { + name = _name; + } + + /// @notice DECs REgistry name getter function + function getName() public 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 - /// @dev the DEC contains sensitive data that must be encrypted - function registerDEC(decData memory dec, address voter) public onlyOwner { + function registerDEC(address dec, address voter) public onlyOwner { require( - registry[voter].length == 0, + registry[voter] == address(0), "The Voter's DEC has been already registered" ); - registry[voter] = encryptDEC(dec); + registry[voter] = 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) { + function getDEC(address voter) public view returns (address) { require( - registry[voter].length != 0, + registry[voter] != address(0), "The Voter don't have a registered DEC" ); return registry[voter]; @@ -54,9 +75,8 @@ contract DECsRegistry is DEC { 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 { + /// @notice this function put the election stamp on the Voter's stamps list after the vote + function stamps(address election, address voter) public onlyOwner { electoralStamps[voter].push(election); emit DECStamped(election, voter); return; diff --git a/script/create-voter-eoa.test.ts b/election-scripts/create-voter-eoa.test.ts similarity index 100% rename from script/create-voter-eoa.test.ts rename to election-scripts/create-voter-eoa.test.ts diff --git a/script/create-voter-eoa.ts b/election-scripts/create-voter-eoa.ts similarity index 100% rename from script/create-voter-eoa.ts rename to election-scripts/create-voter-eoa.ts diff --git a/script/types.ts b/election-scripts/types.ts similarity index 100% rename from script/types.ts rename to election-scripts/types.ts diff --git a/hardhat.config.ts b/hardhat.config.ts index 5c5b4fd..beedefd 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -1,6 +1,7 @@ import { HardhatUserConfig } from "hardhat/config"; import dotenv from "dotenv"; dotenv.config(); +import "@nomicfoundation/hardhat-ignition-ethers"; import "@nomicfoundation/hardhat-toolbox"; import "solidity-coverage"; import "hardhat-gas-reporter"; @@ -8,9 +9,38 @@ import "hardhat-deploy"; const SEPOLIA_URL: string = process.env.SEPOLIA_URL || ""; const ALCHEMY_PRIVATE_KEY: string = process.env.ALCHEMY_PRIVATE_KEY || ""; +const IS_OPTIMIZER_ENABLED = + process.env.NODE_ENV === "production" ? true : false; +const DEBUG = process.env.NODE_ENV === "production" ? "default" : "debug"; const config: HardhatUserConfig = { - solidity: "0.8.24", + solidity: { + version: "0.8.24", + settings: { + optimizer: { + enabled: IS_OPTIMIZER_ENABLED, + runs: 200, + details: { + deduplicate: true, + cse: true, + constantOptimizer: true, + }, + }, + // 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", + debug: { + // How to treat revert (and require) reason strings. Settings are + // "default", "strip", "debug" and "verboseDebug". + // "default" does not inject compiler-generated revert strings and keeps user-supplied ones. + // "strip" removes all revert strings (if possible, i.e. if literals are used) keeping side-effects + // "debug" injects strings for compiler-generated internal reverts, implemented for ABI encoders V1 and V2 for now. + // "verboseDebug" even appends further information to user-supplied revert strings (not yet implemented) + revertStrings: DEBUG, + }, + }, + }, gasReporter: { currency: "EUR", enabled: process.env.REPORT_GAS ? true : false, diff --git a/ignition/modules/DECs.ts b/ignition/modules/DECs.ts new file mode 100644 index 0000000..431ce4a --- /dev/null +++ b/ignition/modules/DECs.ts @@ -0,0 +1,14 @@ +import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; + +export default buildModule("DEC", (m) => { + const DEC1 = m.contract("TomsDEC", [ + "RSSMRA85C27H501W", + "Ardea", + "Lazio", + "Italy", + ]); + + m.call(DEC1, "getName", []); + + return { DEC1 }; +}); diff --git a/ignition/modules/Registries.ts b/ignition/modules/Registries.ts new file mode 100644 index 0000000..4b400ac --- /dev/null +++ b/ignition/modules/Registries.ts @@ -0,0 +1,9 @@ +import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; + +export default buildModule("DECsRegistry", (m) => { + const italianRegistry = m.contract("DECsRegistry", ["Italy"]); + + m.call(italianRegistry, "getName", []); + + return { italianRegistry }; +}); diff --git a/package-lock.json b/package-lock.json index 7254382..0069313 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "agora", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "agora", - "version": "0.1.0", + "version": "0.2.0", "license": "GPL-3.0", "dependencies": { "dotenv": "^16.4.5" @@ -15,6 +15,7 @@ "@commitlint/cli": "^19.2.1", "@commitlint/config-conventional": "^19.1.0", "@commitlint/cz-commitlint": "^19.2.0", + "@nomicfoundation/hardhat-ignition-ethers": "^0.15.0", "@nomicfoundation/hardhat-toolbox": "^5.0.0", "@types/jest": "^29.5.12", "@typescript-eslint/eslint-plugin": "^7.3.1", @@ -3720,7 +3721,6 @@ "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-ignition-ethers/-/hardhat-ignition-ethers-0.15.0.tgz", "integrity": "sha512-KmMNUc/jptfwdPA9ukQf+Ajon+m2vLBjDL2ze7d/vQdrS+fDxmoVwmbbEk4GOjianZcwgQOWD9dEWaj04QiowA==", "dev": true, - "peer": true, "peerDependencies": { "@nomicfoundation/hardhat-ethers": "^3.0.4", "@nomicfoundation/hardhat-ignition": "^0.15.0", diff --git a/package.json b/package.json index c3d5495..bec8fa4 100644 --- a/package.json +++ b/package.json @@ -18,13 +18,16 @@ }, "scripts": { "commit": "./script/commit", + "compile": "npx hardhat compile", "coverage-contracts": "npx hardhat coverage", + "deploy-contract": "./script/deploy", + "duplicated": "npx jscpd", + "lint": "npx lint-staged", "prepare": "husky prepare", "prepare-commit": "git-cz", + "node:start": "npx hardhat node", "test-contracts": "npx hardhat test", - "test-scripts": " jest --coverage --bail", - "lint": "npx lint-staged", - "duplicated": "npx jscpd" + "test-scripts": " jest --coverage --bail" }, "repository": { "type": "git", @@ -56,6 +59,7 @@ "@commitlint/cli": "^19.2.1", "@commitlint/config-conventional": "^19.1.0", "@commitlint/cz-commitlint": "^19.2.0", + "@nomicfoundation/hardhat-ignition-ethers": "^0.15.0", "@nomicfoundation/hardhat-toolbox": "^5.0.0", "@types/jest": "^29.5.12", "@typescript-eslint/eslint-plugin": "^7.3.1", diff --git a/script/deploy b/script/deploy new file mode 100755 index 0000000..ea7214c --- /dev/null +++ b/script/deploy @@ -0,0 +1,11 @@ +#!/bin/bash + +if [ "$#" -ne 2 ]; then + echo "Usage: $0 " + exit 1 +fi + +MODULE_PATH="$1" +NETWORK="$2" + +npx hardhat ignition deploy "$MODULE_PATH" --network "$NETWORK" \ No newline at end of file diff --git a/test/DEC.ts b/test/DEC.ts index a543c45..7aa4aac 100644 --- a/test/DEC.ts +++ b/test/DEC.ts @@ -1,35 +1,63 @@ +import { assert } from "chai"; import { ethers } from "hardhat"; -import { expect } from "chai"; +import { DEC } from "../typechain-types/DEC"; -describe("DEC Contract", function () { - let DEC: any; - let decContract: any; - let ownerAddress: any; +describe("DEC Contract", () => { + let dec: DEC; - before(async function () { - DEC = await ethers.getContractFactory("DEC"); - [ownerAddress] = await ethers.getSigners(); + beforeEach(async () => { + const DECFactory = await ethers.getContractFactory("DEC"); + dec = await DECFactory.deploy( + ethers.encodeBytes32String("12345678901"), + ethers.encodeBytes32String("Roma"), + ethers.encodeBytes32String("Lazio"), + ethers.encodeBytes32String("Italia"), + ); }); - beforeEach(async function () { - decContract = await DEC.deploy(); + it("Should set initial data correctly", async () => { + assert.equal( + await dec.owner(), + await (await ethers.provider.getSigner(0)).getAddress(), + ); + assert.equal( + ethers.decodeBytes32String(await dec.getTaxCode()), + "12345678901", + ); + assert.equal( + ethers.decodeBytes32String(await dec.getMunicipality()), + "Roma", + ); + assert.equal(ethers.decodeBytes32String(await dec.getRegion()), "Lazio"); + assert.equal(ethers.decodeBytes32String(await dec.getCountry()), "Italia"); }); - it("should deploy the contract and set the owner", async function () { - expect(await decContract.owner()).to.equal(ownerAddress.address); + it("Should set and get tax code correctly", async () => { + await dec.setTaxCode(ethers.encodeBytes32String("98765432109")); + assert.equal( + ethers.decodeBytes32String(await dec.getTaxCode()), + "98765432109", + ); }); - it("should encrypt DEC data correctly", async function () { - const decData = { - taxCode: "123456789", - municipality: "Sample Municipality", - province: "Sample Province", - region: "Sample Region", - country: "Sample Country", - }; + it("Should set and get municipality correctly", async () => { + await dec.setMunicipality(ethers.encodeBytes32String("Milano")); + assert.equal( + ethers.decodeBytes32String(await dec.getMunicipality()), + "Milano", + ); + }); - const encryptedData = await decContract.encryptDEC(decData); + it("Should set and get region correctly", async () => { + await dec.setRegion(ethers.encodeBytes32String("Lombardia")); + assert.equal( + ethers.decodeBytes32String(await dec.getRegion()), + "Lombardia", + ); + }); - expect(encryptedData.data).to.not.be.null; + it("Should set and get country correctly", async () => { + await dec.setCountry(ethers.encodeBytes32String("Francia")); + assert.equal(ethers.decodeBytes32String(await dec.getCountry()), "Francia"); }); }); diff --git a/test/DECsRegistry.ts b/test/DECsRegistry.ts index 6bebe2c..d98c619 100644 --- a/test/DECsRegistry.ts +++ b/test/DECsRegistry.ts @@ -2,36 +2,39 @@ import { ethers } from "hardhat"; import { expect } from "chai"; import { Signer } from "ethers"; import { DECsRegistry } from "../typechain-types/DECsRegistry"; -import { DecData } from "./types"; +import { generateMockAddress } from "./utils"; 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", - }; + const electionAddress = generateMockAddress(); + const decAddress = generateMockAddress(); beforeEach(async () => { const ContractFactory = await ethers.getContractFactory("DECsRegistry"); [owner, voter] = await ethers.getSigners(); - contract = await ContractFactory.deploy(); + contract = await ContractFactory.deploy("test"); }); it("Should deploy the contract", async function () { expect(contract.address).to.not.equal(0); }); + it("should set and get name correctly", async function () { + const newName = "New Name"; + + await contract.connect(owner).setName(newName); + const retrievedName = await contract.getName(); + + expect(retrievedName).to.equal(newName); + }); + it("Should register DEC", async function () { const response = await contract .connect(owner) - .registerDEC(decData, await voter.getAddress()); + .registerDEC(decAddress, await voter.getAddress()); expect(response.blockHash).to.not.equal(null); expect(response.blockHash).to.not.equal(undefined); @@ -41,16 +44,16 @@ describe("DECs Registry Contract", function () { it("Should not register DEC if already registered", async function () { await contract .connect(owner) - .registerDEC(decData, await voter.getAddress()); + .registerDEC(decAddress, await voter.getAddress()); await expect( - contract.connect(owner).registerDEC(decData, await voter.getAddress()), + contract.connect(owner).registerDEC(decAddress, 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()); + .registerDEC(decAddress, await voter.getAddress()); const retrievedDEC = await contract.getDEC(await voter.getAddress()); expect(retrievedDEC.length).to.be.greaterThan(0); @@ -65,7 +68,7 @@ describe("DECs Registry Contract", function () { it("Should return true if voter already voted", async function () { await contract .connect(owner) - .stampsTheDEC(electionAddress, await voter.getAddress()); + .stamps(electionAddress, await voter.getAddress()); const hasVoted = await contract.hasVoterAlreadyVoted( await voter.getAddress(), diff --git a/test/types.ts b/test/types.ts deleted file mode 100644 index 4b561a1..0000000 --- a/test/types.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface DecData { - taxCode: string; - municipality: string; - province: string; - region: string; - country: string; -} diff --git a/test/utils.ts b/test/utils.ts new file mode 100644 index 0000000..5e015e0 --- /dev/null +++ b/test/utils.ts @@ -0,0 +1,11 @@ +import { ethers } from "hardhat"; + +/** + * This function generates a mock address + * + * @returns {string} - a random mock address + */ +export function generateMockAddress(): string { + const wallet = ethers.Wallet.createRandom(); + return wallet.address; +}