Quickstart Guide to Develop on zkSync : Build a Complete Voting DApp on the ZKSync (Chapter 2)

Ridho IzzulhaqRidho Izzulhaq
6 min read

In the previous chapter, we delivered an instant way related to Concept and Development with ZKSync and Remix IDE through the Nethermind plugin. In this tutorial, we will discuss how to build a complete Voting DApp on the ZKSync network. We will begin by creating a basic smart contract. This contract will include functions for vote and viewing the voting result. We will use ATLAS IDE to deploy the contract onto the ZKSync network. Finally, we will develop a frontend interface to interact with our smart contract, allowing users to perform operations through a web application.

Let's Build the Smart Contract

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

contract Voting {
    struct Candidate {
        uint id;
        string name;
        uint voteCount;
    }

    mapping(uint => Candidate) public candidates;
    mapping(address => bool) public voters;
    uint public candidatesCount;


    constructor(string[] memory _candidateNames) {
        for (uint i = 0; i < _candidateNames.length; i++) {
            addCandidate(_candidateNames[i]);
        }
    }

    function addCandidate(string memory _name) private {
        candidatesCount++;
        candidates[candidatesCount] = Candidate(candidatesCount, _name, 0);
    }

    function vote(uint _candidateId) public {
        require(!voters[msg.sender], "You have already voted.");
        require(_candidateId > 0 && _candidateId <= candidatesCount, "Invalid candidate ID.");

        voters[msg.sender] = true;
        candidates[_candidateId].voteCount++;
    }

    function getVotes(uint _candidateId) public view returns (uint) {
        require(_candidateId > 0 && _candidateId <= candidatesCount, "Invalid candidate ID.");
        return candidates[_candidateId].voteCount;
    }
}

The contract's name is Voting, and it includes a struct named Candidate that stores information about each candidate, including their id, name, and vote count.

 struct Candidate {
        uint id;
        string name;
        uint voteCount;
    }

constructor constructor(string[] memory _candidateNames) is used to initialize the contract with an array of candidate names.

  constructor(string[] memory _candidateNames) {
        for (uint i = 0; i < _candidateNames.length; i++) {
            addCandidate(_candidateNames[i]);
        }
    }

There are three functions, namely :

addCandidate(string memory _name) private: function is used to add a new candidate to the voting contract. It takes a string _name as a parameter, which represents the name of the candidate to be added. The function first increments the candidatesCount variable to generate a unique ID for the candidate. It then creates a new Candidate struct with the provided _name, assigns the incremented candidatesCount as the candidate's ID, and sets the initial voteCount to 0. Finally, it stores this new candidate in the candidates mapping using the candidate's ID as the key.

vote(uint _candidateId) public: function is used by a voter to cast a vote for a specific candidate. It takes a parameter _candidateId representing the ID of the candidate the voter wants to vote for. The function first checks that the voter has not already voted by ensuring voters[msg.sender] is false. It then checks that the provided _candidateId is valid, i.e., greater than 0 and less than or equal to candidatesCount. If both conditions are met, the function marks the voter as having voted (voters[msg.sender] = true) to prevent multiple votes from the same address and increments the voteCount of the candidate with the specified _candidateId.

function getVotes(uint _candidateId) public view returns (uint): This declares a public function named getVotes that takes a single parameter _candidateId of type uint (unsigned integer). It is marked as view, indicating that it does not modify the state of the blockchain and only reads data. The function returns a uint, which will be the vote count for the specified candidate.

   function addCandidate(string memory _name) private {
        candidatesCount++;
        candidates[candidatesCount] = Candidate(candidatesCount, _name, 0);
    }

    function vote(uint _candidateId) public {
        require(!voters[msg.sender], "You have already voted.");
        require(_candidateId > 0 && _candidateId <= candidatesCount, "Invalid candidate ID.");

        voters[msg.sender] = true;
        candidates[_candidateId].voteCount++;
    }  

 function getVotes(uint _candidateId) public view returns (uint) {
        require(_candidateId > 0 && _candidateId <= candidatesCount, "Invalid candidate ID.");
        return candidates[_candidateId].voteCount;
    }

Deploy the Code with Atlas IDE

If in the previous chapter we used the Nethermind plugin on REMIX IDE, in this stage we will use ATLAS IDE and the Sepolia Testnet via Metamask Wallet.

Simply create a new blank project in ATLAS IDE and then insert the smart contract provided above.

After that, in the top right corner under the network section, use zkSync Sepolia testnet and then connect it with Metamask. Make sure you have filled your testnet wallet using the faucet beforehand.

Next, you just need to insert the constructor as defined in the smart contract :

This constructor will then be used in the addCandidate function, which adds a candidate to the contract by incrementing candidatesCount to obtain an ID. When the first candidate is added, addCandidate increases the value of candidatesCount to 1 (for the first candidate), which is then used as the unique ID for that candidate. Each new candidate added will have a higher candidatesCount as their unique ID. Example:

We can view the deployed contract on the explorer.

At this step, you can now vote using the vote function and track the votes for each candidate using their ID.

Build Web Interaction with Web3.js and React

To create a DApp for voting with Web3.js and React, follow these steps:

  1. Setup React App:

    • Create a new React app using Create React App: npx create-react-app voting-dapp

    • Navigate into the app directory: cd voting-dapp

  2. Install Web3.js:

    • Install Web3.js package: npm install web3
  3. Modify App.js with code

import React, { useState, useEffect } from 'react';
import Web3 from 'web3';

const contractAddress = ''; //field with project contract address
const abi = [
  //field with our project ABI
];
const web3 = new Web3(window.ethereum);
const contract = new web3.eth.Contract(abi, contractAddress);

function App() {
  const [candidates, setCandidates] = useState([]);
  const [votes, setVotes] = useState([]);

  useEffect(() => {
    async function fetchData() {
      const numCandidates = await contract.methods.candidatesCount().call();
      const candidates = [];
      const votes = [];
      for (let i = 1; i <= numCandidates; i++) {
        const candidate = await contract.methods.candidates(i).call();
        const vote = await contract.methods.getVotes(i).call();
        candidates.push(candidate.name);
        votes.push(vote);
      }
      setCandidates(candidates);
      setVotes(votes);
    }
    fetchData();
  }, []);

  async function vote(candidateIndex) {
    const accounts = await web3.eth.getAccounts();
    await contract.methods.vote(candidateIndex).send({ from: accounts[0] });
    // Refresh data after voting
    const votes = [];
    for (let i = 1; i <= candidates.length; i++) {
      const vote = await contract.methods.getVotes(i).call();
      votes.push(vote);
    }
    setVotes(votes);
  }

  return (
    <div className="App">
      <h1>List of Candidates</h1>
      <ul>
        {candidates.map((candidate, index) => (
          <li key={index}>
            {candidate} - Votes: {votes[index]}
            <button onClick={() => vote(index + 1)}>Vote</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default App;

Interact with Our Simple dAPP

Closing

Thank you for your support and interest in this series of articles, which is part of a grant program available at Wave Hacks on Akindo.

Structure :

WAVETITLELINK
1stQuickstart Guide to Develop on zkSync : Fundamental Concept and Development with ZKSync Remix IDE 'Instant Way' (Chapter 1)https://ridhoizzulhaq.hashnode.dev/quickstart-guide-to-develop-on-zksync-fundamental-concept-and-development-with-zksync-remix-ide-instant-way-chapter-1
3rdQuickstart Guide to Develop on zkSync : Build a Complete Voting DApp on the ZKSync (Chapter 2)https://ridhoizzulhaq.hashnode.dev/quickstart-guide-to-develop-on-zksync-build-a-complete-voting-dapp-on-the-zksync-chapter-2
4thQuickstart Guide to Develop on ZKSync : Explore Basic Paymaster (Chapter 3)https://ridhoizzulhaq.hashnode.dev/quickstart-guide-to-develop-on-zksync-explore-basic-paymaster-chapter-3
5thQuickstart Guide to Develop on ZKSync : Build NFT ERC-1155 Ticket Minter with Paymaster feature and DApp for Ticket Checker (Chapter 4)https://ridhoizzulhaq.hashnode.dev/quickstart-guide-to-develop-on-zksync-build-nft-erc-1155-ticket-minter-with-paymaster-feature-and-dapp-for-ticket-checker-chapter-4
0
Subscribe to my newsletter

Read articles from Ridho Izzulhaq directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ridho Izzulhaq
Ridho Izzulhaq