A decentralized voting system built with Ethereum smart contracts and a Node.js backend. This system allows users to vote for candidates in a transparent, secure, and immutable manner on the blockchain.
- Overview
- Features
- Smart Contract Logic
- Backend Architecture
- Prerequisites
- Installation
- Usage
- Testing
- API Endpoints
- Project Structure
This project implements a complete decentralized voting system with:
- Smart Contract: Solidity contract deployed on Ethereum (local Hardhat network)
- Backend API: Node.js/Express REST API that interacts with the smart contract using Web3.js
- Testing: Comprehensive test suite for both smart contract and backend integration
- β Only contract owner (admin) can add candidates
- β One vote per Ethereum address
- β View all candidates with vote counts
- β Declare winner (candidate with most votes)
- β Gas-efficient custom errors
- β Event emissions for transparency
- β Interface-based design for better code organization
- β RESTful API endpoints
- β Input validation middleware
- β Error handling middleware
- β Request logging
- β Rate limiting
- β CORS support
- β Automatic contract initialization from deployment artifacts
The VotingSystem contract implements a decentralized voting mechanism with the following key components:
address public immutable admin; // Contract owner (set at deployment)
Candidate[] public candidates; // Array of all candidates
mapping(address => bool) public hasVoted; // Tracks if an address has voted
mapping(address => uint256) public voterToCandidate; // Maps voter to their chosen candidate-
addCandidate(string memory name)(Admin Only)- Adds a new candidate to the voting system
- Protected by
onlyAdminmodifier - Validates that name is not empty
- Emits
CandidateAddedevent - Reverts with:
EmptyCandidateNameif name is empty,Unauthorizedif caller is not admin
-
vote(uint256 candidateIndex)- Allows an address to cast a vote for a candidate
- Enforces one-vote-per-address rule
- Validates candidate index exists
- Updates vote count and tracking mappings
- Emits
VoteCastevent - Reverts with:
AlreadyVotedif address already voted,InvalidCandidateIndexif index is invalid
-
getCandidates()(View)- Returns array of all candidates with their vote counts
- No gas cost (view function)
-
getWinner()(View)- Returns the name of the candidate with the most votes
- In case of a tie, returns the first candidate with max votes
- Reverts with:
NoCandidatesif no candidates exist
-
getCandidateCount()(View)- Returns the total number of candidates
-
hasAddressVoted(address _voter)(View)- Checks if a specific address has already voted
- Access Control:
onlyAdminmodifier ensures only the contract owner can add candidates - Vote Protection:
hasVotedmapping prevents double voting - Input Validation: Custom errors for invalid inputs (gas-efficient)
- Immutable Admin: Admin address cannot be changed after deployment
- Uses custom errors instead of
requirestatements (saves gas) - Efficient storage patterns (mappings for O(1) lookups)
- Events for off-chain tracking instead of storing redundant data
backend/
βββ server.js # Express app setup and middleware configuration
βββ web3Client.js # Web3 initialization and contract instance creation
βββ routes/
β βββ voting.js # API route handlers for voting operations
βββ middleware/
β βββ validator.js # Input validation middleware
β βββ errorHandler.js # Centralized error handling
β βββ logger.js # Request logging middleware
βββ test-api.js # Integration test script
- Initializes Web3 connection to Hardhat local node
- Loads contract ABI from Hardhat Ignition deployment artifacts
- Loads deployed contract address from
deployed_addresses.json - Creates contract instance for interaction
- Auto-initializes on module load
- POST
/api/candidates: Add a new candidate (admin only) - GET
/api/candidates: Get all candidates with vote counts - POST
/api/vote: Cast a vote for a candidate - GET
/api/winner: Get the current winner
Validator (middleware/validator.js):
validateContract: Ensures contract is initializedvalidateAddCandidate: Validates candidate name (type, length, non-empty)validateVote: Validates voter address format and checks if already voted
Error Handler (middleware/errorHandler.js):
- Centralized error handling
- Parses blockchain-specific errors (revert, insufficient funds, etc.)
- Returns consistent error response format
Logger (middleware/logger.js):
- Logs all incoming requests with status codes
- Express app setup
- CORS enabled
- Rate limiting (100 requests per minute)
- JSON body parsing
- Request logging with Morgan
- Error handling middleware
- Node.js 18+ (for native fetch support)
- npm or yarn
- Basic understanding of Ethereum and Solidity
-
Clone the repository (if applicable) or navigate to the project directory
-
Install dependencies:
npm install- Environment Setup (Optional):
Create a
.envfile in the root directory:
RPC_URL=http://127.0.0.1:8545
PORT=3000
GAS_LIMIT=500000In a terminal, start the Hardhat local blockchain:
npm run node
# or
npx hardhat nodeThis will:
- Start a local Ethereum node on
http://127.0.0.1:8545 - Provide 20 test accounts with pre-funded ETH
- Keep running until you stop it (Ctrl+C)
In a new terminal, deploy the contract to the local network:
npx hardhat ignition deploy ignition/modules/VotingSystem.js --network localhostThis will:
- Deploy the
VotingSystemcontract - Save the deployment address to
ignition/deployments/chain-31337/deployed_addresses.json - Save the contract artifact to
ignition/deployments/chain-31337/artifacts/
Note: The contract address will be displayed in the terminal. The backend automatically loads this address.
In a new terminal, start the Express backend:
npm run backendThe server will:
- Start on
http://localhost:3000(or PORT from .env) - Initialize Web3 connection
- Load contract ABI and address
- Be ready to accept API requests
In a new terminal, run the integration tests:
node backend/test-api.jsThis script will:
- Add candidates (Alice, Bob, Charlie)
- Cast votes from different addresses
- Test double-vote prevention
- Get updated vote counts
- Declare the winner
Run the Hardhat test suite:
npm test
# or
npx hardhat testThis runs all tests in test/VotingSystem.js, including:
- Deployment tests
- Add candidate tests (admin and non-admin)
- Vote functionality tests
- Double-vote prevention tests
- Winner declaration tests
- Custom error tests
- Event emission tests
- Integration scenarios
Run the API integration test script:
node backend/test-api.jsPrerequisites:
- Hardhat node running (
npm run node) - Contract deployed (
npx hardhat ignition deploy ignition/modules/VotingSystem.js --network localhost) - Backend server running (
npm run backend)
The test script will:
- Get available accounts from Web3
- Add 3 candidates (Alice, Bob, Charlie)
- Get all candidates
- Cast votes from different addresses
- Test double-vote prevention
- Get updated vote counts
- Get the winner
POST /api/candidates
Content-Type: application/json
{
"name": "Alice"
}Response (201 Created):
{
"success": true,
"message": "Candidate added successfully",
"data": {
"name": "Alice",
"candidateIndex": 0,
"transactionHash": "0x..."
}
}Errors:
400: Invalid input (empty name, wrong type, etc.)500: Admin address not available503: Contract not initialized
GET /api/candidatesResponse (200 OK):
{
"success": true,
"data": {
"candidates": [
{
"index": 0,
"name": "Alice",
"voteCount": 2
},
{
"index": 1,
"name": "Bob",
"voteCount": 1
}
],
"totalCandidates": 2
}
}POST /api/vote
Content-Type: application/json
{
"voterAddress": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
"candidateIndex": 0
}Response (200 OK):
{
"success": true,
"message": "Vote cast successfully",
"data": {
"voterAddress": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
"candidateIndex": 0,
"candidateName": "Alice",
"transactionHash": "0x..."
}
}Errors:
400: Invalid address, invalid index, already voted500: Transaction failed
GET /api/winnerResponse (200 OK):
{
"success": true,
"data": {
"winner": {
"name": "Alice",
"voteCount": 2
}
}
}Response (No votes cast):
{
"success": true,
"message": "No winner yet (no votes cast)",
"data": {
"winner": null
}
}GET /healthResponse:
{
"status": "ok",
"message": "Voting System API is running"
}voting_system/
βββ contracts/
β βββ VotingSystem.sol # Main voting contract
β βββ IVotingSystem.sol # Contract interface
βββ test/
β βββ VotingSystem.js # Smart contract tests
βββ backend/
β βββ server.js # Express server
β βββ web3Client.js # Web3 initialization
β βββ test-api.js # API integration tests
β βββ routes/
β β βββ voting.js # API routes
β βββ middleware/
β βββ validator.js # Input validation
β βββ errorHandler.js # Error handling
β βββ logger.js # Request logging
βββ ignition/
β βββ modules/
β βββ VotingSystem.js # Deployment module
βββ hardhat.config.js # Hardhat configuration
βββ package.json # Dependencies and scripts
βββ README.md # This file
Here's the complete workflow to get started:
# Terminal 1: Start Hardhat node
npm run node
# Terminal 2: Deploy contract
npx hardhat ignition deploy ignition/modules/VotingSystem.js --network localhost
# Terminal 3: Start backend
npm run backend
# Terminal 4: Run integration tests
node backend/test-api.jsnpm run node- Start Hardhat local nodenpm test- Run smart contract testsnpm run backend- Start the backend server
The contract is deployed using Hardhat Ignition. The deployment artifacts are automatically saved to:
ignition/deployments/chain-31337/deployed_addresses.json- Contract addressignition/deployments/chain-31337/artifacts/- Contract ABI and bytecode
The backend automatically loads these artifacts on startup.
- The Hardhat local node maintains state across restarts unless you reset it
- If you restart the Hardhat node, you need to redeploy the contract
- The backend server must be restarted after redeployment to load the new contract address
- All transactions on the local Hardhat node are instant and free (no real gas costs)
- Ensure the contract is deployed:
npx hardhat ignition deploy ignition/modules/VotingSystem.js --network localhost - Check that
ignition/deployments/chain-31337/deployed_addresses.jsonexists - Restart the backend server after deployment
- Kill existing processes:
lsof -ti :8545 | xargs kill -9(Hardhat node) - Kill existing processes:
lsof -ti :3000 | xargs kill -9(Backend server)
- Ensure the Hardhat node is running
- Check that the backend middleware is checking
hasVotedbefore allowing votes - Restart the Hardhat node and redeploy if state is corrupted
Built with: Solidity, Hardhat, Node.js, Express, Web3.js