Secure Blockchain Voting System Solidity, JavaScript

👤 Sharing: AI
Okay, here's a basic example of a secure blockchain voting system using Solidity (for the smart contract) and JavaScript (for the front-end interaction).  I'll include explanations throughout to help you understand the code.  Keep in mind that this is a simplified example for illustrative purposes. A real-world secure voting system would require significantly more robust security measures and testing.

**Solidity (Smart Contract - `Voting.sol`)**

```solidity
pragma solidity ^0.8.0;

contract Voting {
    // Struct to represent a candidate
    struct Candidate {
        string name;
        uint voteCount;
    }

    // Mapping from candidate index to Candidate struct
    mapping(uint => Candidate) public candidates;

    // Number of candidates
    uint public candidateCount;

    // Mapping to track who has voted (address => bool)
    mapping(address => bool) public voters;

    // Event emitted when a vote is cast
    event Voted(address indexed voter, uint indexed candidateId);

    // Modifier to restrict access to only the contract owner
    address public owner;

    constructor(string[] memory _candidateNames) {
        owner = msg.sender; // Set the owner to the deployer
        candidateCount = _candidateNames.length;
        for (uint i = 0; i < _candidateNames.length; i++) {
            candidates[i + 1] = Candidate(_candidateNames[i], 0);  // Start index at 1 for readability
        }
    }


    // Function to cast a vote
    function vote(uint _candidateId) public {
        //require(msg.sender == owner, "Only the owner can add candidates.");
        require(!voters[msg.sender], "You have already voted.");
        require(_candidateId > 0 && _candidateId <= candidateCount, "Invalid candidate ID.");

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

        emit Voted(msg.sender, _candidateId);
    }

    // Function to get the vote count for a candidate
    function getVoteCount(uint _candidateId) public view returns (uint) {
        require(_candidateId > 0 && _candidateId <= candidateCount, "Invalid candidate ID.");
        return candidates[_candidateId].voteCount;
    }

    // Function to get the name of a candidate
    function getCandidateName(uint _candidateId) public view returns (string memory) {
        require(_candidateId > 0 && _candidateId <= candidateCount, "Invalid candidate ID.");
        return candidates[_candidateId].name;
    }

    function hasVoted(address _voter) public view returns (bool) {
        return voters[_voter];
    }


}
```

**Explanation (Solidity):**

*   **`pragma solidity ^0.8.0;`**:  Specifies the Solidity compiler version.  It's important to use a specific version to avoid compatibility issues.
*   **`contract Voting { ... }`**: Defines the smart contract named `Voting`.
*   **`struct Candidate { ... }`**:  Defines a structure to hold information about each candidate (name and vote count).
*   **`mapping(uint => Candidate) public candidates;`**: A mapping that associates a candidate ID (an unsigned integer) with the `Candidate` struct.  The `public` keyword automatically creates a getter function.
*   **`uint public candidateCount;`**:  Stores the total number of candidates.
*   **`mapping(address => bool) public voters;`**:  A mapping that keeps track of which Ethereum addresses have already voted.  This prevents double-voting.
*   **`event Voted(address indexed voter, uint indexed candidateId);`**:  An event that is emitted when a vote is successfully cast.  Events allow external applications (like your JavaScript front-end) to listen for and react to changes in the smart contract's state.  The `indexed` keyword makes it easier to filter events by specific addresses or candidate IDs.
*   **`constructor(string[] memory _candidateNames) { ... }`**:  The constructor is executed only once when the contract is deployed. It initializes the `candidateCount` and populates the `candidates` mapping with the names provided.
*   **`function vote(uint _candidateId) public { ... }`**:  This function allows a user to cast their vote for a specific candidate.
    *   `require(!voters[msg.sender], "You have already voted.");`:  Checks if the voter has already voted. `msg.sender` is the address of the caller.
    *   `require(_candidateId > 0 && _candidateId <= candidateCount, "Invalid candidate ID.");`: Validates the candidate ID.
    *   `candidates[_candidateId].voteCount++;`: Increments the vote count for the selected candidate.
    *   `voters[msg.sender] = true;`: Marks the voter as having voted.
    *   `emit Voted(msg.sender, _candidateId);`: Emits the `Voted` event.
*   **`function getVoteCount(uint _candidateId) public view returns (uint) { ... }`**: Returns the current vote count for a given candidate ID.  The `view` keyword indicates that this function doesn't modify the contract's state.
*   **`function getCandidateName(uint _candidateId) public view returns (string memory) { ... }`**: Returns the name of a candidate given their ID.
*   **`function hasVoted(address _voter) public view returns (bool) { ... }`**: Returns whether a particular address has already voted or not.

**JavaScript (Front-End - `index.html` and `app.js`)**

**`index.html`**

```html
<!DOCTYPE html>
<html>
<head>
    <title>Blockchain Voting System</title>
    <script src="https://cdn.jsdelivr.net/npm/web3@1.6.0/dist/web3.min.js"></script>
    <style>
        body {
            font-family: sans-serif;
        }
        .container {
            width: 600px;
            margin: 20px auto;
            padding: 20px;
            border: 1px solid #ccc;
        }
        button {
            padding: 10px 20px;
            background-color: #4CAF50;
            color: white;
            border: none;
            cursor: pointer;
        }
        button:disabled {
          background-color: #cccccc;
          color: #666666;
          cursor: not-allowed;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>Blockchain Voting</h1>

        <div id="candidates">
            <!-- Candidate buttons will be dynamically added here -->
        </div>

        <p id="account">Account: <span id="accountAddress"></span></p>
        <p id="hasVotedMessage" style="color: red; display: none;">You have already voted.</p>
        <p id="voteMessage"></p>

    </div>

    <script src="app.js"></script>
</body>
</html>
```

**`app.js`**

```javascript
App = {
    web3Provider: null,
    contracts: {},
    account: '0x0',
    hasVoted: false,

    init: async function() {
        return App.initWeb3();
    },

    initWeb3: async function() {
        // Modern dapp browsers...
        if (window.ethereum) {
            App.web3Provider = window.ethereum;
            try {
                // Request account access
                await window.ethereum.enable();
            } catch (error) {
                // User denied account access...
                console.error("User denied account access")
            }
        }
        // Legacy dapp browsers...
        else if (window.web3) {
            App.web3Provider = window.web3.currentProvider;
        }
        // If no injected web3 instance is detected, fall back to Ganache
        else {
            App.web3Provider = new Web3.providers.HttpProvider('http://localhost:7545');
        }
        web3 = new Web3(App.web3Provider);

        return App.initContract();
    },

    initContract: function() {
        $.getJSON("Voting.json", function(voting) {
            // Instantiate a new truffle contract from the artifact
            App.contracts.Voting = TruffleContract(voting);
            // Connect provider to interact with contract
            App.contracts.Voting.setProvider(App.web3Provider);

            App.listenForEvents();
            return App.render();
        });
    },

    listenForEvents: function() {
        App.contracts.Voting.deployed().then(function(instance) {
            instance.Voted({}, {
                fromBlock: 0,
                toBlock: 'latest'
            }).watch(function(error, event) {
                console.log("event triggered", event)
                // Reload when a new vote is recorded
                App.render();
            });
        });
    },

    render: async function() {
        var votingInstance;
        var loader = $("#loader");
        var content = $("#content");

        // Load account data
        web3.eth.getCoinbase(function(err, account) {
            if (err === null) {
                App.account = account;
                $("#accountAddress").html(account);
            }
        });

        // Load contract data
        App.contracts.Voting.deployed().then(function(instance) {
            votingInstance = instance;
            return votingInstance.candidateCount();
        }).then(function(candidateCount) {
            var candidatesResults = $("#candidates");
            candidatesResults.empty();

            var candidatesSelect = $('#candidatesSelect');
            candidatesSelect.empty();

            for (var i = 1; i <= candidateCount; i++) {
                votingInstance.candidates(i).then(function(candidate) {
                    var id = candidate[0];
                    var name = candidate[1];
                    var voteCount = candidate[2];

                    // Render candidate Result
                    var candidateTemplate = "<div>" + name + ": " + voteCount + "</div>";
                    candidatesResults.append(candidateTemplate);

                    // Render candidate ballot option
                    var candidateOption = "<button id='candidate-" + id + "' class='vote-button' data-id='" + id + "'>" + name + "</button><br>";
                    candidatesResults.append(candidateOption);

                    // Event listener for vote buttons
                    $('.vote-button').on('click', function(event) {
                      event.preventDefault();
                      var candidateId = $(this).data('id');
                      App.castVote(candidateId);
                    });

                });
            }
            return votingInstance.voters(App.account);
        }).then(function(hasVoted) {
            // Do not allow a user to vote
            if(hasVoted) {
              $('vote-button').prop('disabled', true);
              $('#hasVotedMessage').show();
            } else {
                $('vote-button').prop('disabled', false);
                $('#hasVotedMessage').hide();
            }
        }).catch(function(error) {
            console.warn(error);
        });
    },

    castVote: function(candidateId) {
        var votingInstance;

        App.contracts.Voting.deployed().then(function(instance) {
            votingInstance = instance;
            return votingInstance.vote(candidateId, { from: App.account });
        }).then(function(result) {
            // Wait for votes to update
            $("#voteMessage").text("Vote submitted...");
            App.render();
        }).catch(function(err) {
            console.error(err);
            $("#voteMessage").text("Error submitting vote.");
        });
    }
};

$(function() {
    $(window).load(function() {
        App.init();
    });
});
```

**Explanation (JavaScript):**

1.  **`App` Object:** An object to encapsulate all the application logic.
2.  **`web3Provider`:** Stores the web3 provider (MetaMask, Ganache, etc.).
3.  **`contracts`:** An object to hold the smart contract instance.
4.  **`account`:** Stores the user's Ethereum address.
5.  **`hasVoted`:** A boolean to track whether the user has already voted.
6.  **`initWeb3()`:** Initializes web3:
    *   Detects the web3 provider (MetaMask, Ganache, etc.).
    *   Requests account access (if using MetaMask).
    *   Creates a web3 instance.
7.  **`initContract()`:**
    *   Loads the `Voting.json` artifact (generated by Truffle during compilation).
    *   Instantiates a Truffle contract object.
    *   Sets the web3 provider for the contract.
    *   Calls `render()` to display the initial data.
    *   Calls `listenForEvents` to listen for contract events.
8.  **`listenForEvents()`:**
    *   Listens for the `Voted` event emitted by the smart contract.
    *   When a `Voted` event is received, it re-renders the UI to update the vote counts.
9.  **`render()`:**  Responsible for displaying the data from the smart contract on the page.
    *   Gets the user's Ethereum account using `web3.eth.getCoinbase`.
    *   Loads the candidate count and then iterates through the candidates.
    *   For each candidate, it gets the candidate's name and vote count.
    *   Updates the HTML with the candidate information.
    *   Checks if the current user has already voted.
    *   Disables the vote buttons if the user has already voted.
10. **`castVote(candidateId)`:** Called when a user clicks a vote button.
    *   Calls the `vote()` function on the smart contract.
    *   Sends the transaction from the user's account.
    *   Displays a message indicating that the vote has been submitted.
    *   Re-renders the UI to update the vote counts.

**Steps to Run:**

1.  **Install Truffle and Ganache:**

    ```bash
    npm install -g truffle
    npm install -g ganache-cli  # Older, but Ganache UI is easier for beginners
    # OR
    npm install -g ganache # Newer command-line version
    ```

2.  **Create a Project Directory:**

    ```bash
    mkdir voting-app
    cd voting-app
    truffle init
    ```

3.  **Place Solidity Code:** Create a `contracts` directory (if it doesn't exist) and place the `Voting.sol` file inside.

4.  **Create Migration File:**  Create a file named `2_deploy_contracts.js` in the `migrations` directory with the following content:

    ```javascript
    const Voting = artifacts.require("Voting");

    module.exports = function(deployer) {
      const candidateNames = ["Candidate 1", "Candidate 2", "Candidate 3"]; // Example candidates
      deployer.deploy(Voting, candidateNames);
    };
    ```

5.  **Configure Truffle (truffle-config.js or truffle-config.js):** Make sure your `truffle-config.js` or `truffle.js` file (depending on your Truffle version) is configured to connect to Ganache. A basic configuration might look like this:

    ```javascript
    module.exports = {
      networks: {
        development: {
          host: "127.0.0.1",
          port: 7545, // Default Ganache port
          network_id: "*" // Match any network id
        }
      },
      compilers: {
        solc: {
          version: "0.8.0", // Or the version you're using
        }
      }
    };
    ```

6.  **Start Ganache:** Open Ganache (the GUI version) or run `ganache-cli` in your terminal.

7.  **Compile and Deploy:**

    ```bash
    truffle compile
    truffle migrate
    ```

8.  **Create Front-End Files:** Create an `index.html` and `app.js` file in the root directory of your project. Make sure the file `Voting.json` has been generated in the `build/contracts` folder.

9.  **Serve the Front-End:**  You can use a simple HTTP server (like `http-server` from npm) to serve the `index.html` file.

    ```bash
    npm install -g http-server
    http-server .
    ```

10. **Open in Browser:**  Open your browser and go to the address provided by `http-server` (usually `http://localhost:8080`).

11. **Connect MetaMask:** If you're using MetaMask, connect it to the Ganache network (usually a custom RPC with `http://localhost:7545`). Import one of the accounts from Ganache into MetaMask to use as your voting account.

**Important Considerations and Security Notes:**

*   **Security Audits:**  This is a *very* basic example and *should not* be used in a real-world voting scenario without a professional security audit.
*   **Authentication/Authorization:** This example uses a simple "one vote per address" mechanism.  Real voting systems need robust authentication (verifying the identity of the voter) and authorization (ensuring only eligible voters can vote).
*   **Ballot Secrecy:**  This example does not address ballot secrecy.  Votes are publicly visible on the blockchain.  You would need to use advanced cryptographic techniques (e.g., zero-knowledge proofs, homomorphic encryption) to protect voter privacy.
*   **Decentralization:** The constructor takes in an array of candidate names. This makes the contract owner the only person who can create new candidates. A truly decentralized application should allow anyone to submit a proposal for a new candidate.
*   **Random Number Generation:**  If you need randomness (e.g., for selecting winners in a lottery), do *not* use `block.timestamp` or `blockhash` directly. They are predictable by miners.  Use a secure, verifiable source of randomness (e.g., Chainlink VRF).
*   **Gas Costs:**  Blockchain transactions cost gas.  Optimize your smart contract code to minimize gas usage.
*   **Testing:** Thoroughly test your smart contract with unit tests and integration tests.  Use tools like Truffle and Ganache for testing.
*   **Upgradability:**  Smart contracts are generally immutable.  Design your contracts to be upgradable (using proxy patterns) if you anticipate needing to make changes in the future.  This adds complexity.
*   **Frontend Security:** Secure your front-end against common web vulnerabilities (XSS, CSRF, etc.).
*   **Oracle Services:** If you need to fetch data from outside the blockchain (e.g., voter registration lists), use a reliable oracle service like Chainlink.
*   **DoS Attacks:**  Protect against denial-of-service attacks (e.g., someone spamming the contract with transactions).  Use techniques like rate limiting and gas limits.
*   **Governance:** Consider a governance mechanism for making changes to the voting system itself (e.g., adding new features, changing voting rules).

This example provides a starting point. Building a truly secure and reliable blockchain voting system is a complex engineering challenge.
👁️ Viewed: 10

Comments