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