A privacy-preserving voting system that enables secure and anonymous voting while maintaining transparency in the vote counting process. The system uses Pedersen commitments for vote privacy and implements economic incentives to ensure proper participation.
- Privacy preservation through homomorphic vote commitments
- Role-based participation (voters, submitters, and talliers)
- Economic incentives and slashing mechanisms
- Stake-based security model
- Merkle tree-based vote verification
- Upgradeable smart contract architecture
The system uses Pedersen commitments to enable private voting while maintaining verifiability. A Pedersen commitment is a cryptographic primitive that allows a user to commit to a value while keeping it hidden, with the ability to reveal it later.
For a vote v (true or false) and a random blinding factor r, the commitment is calculated as:
C = v*G + r*H
where:
GandHare generator points on an elliptic curve (we use the same generator points as Tornado Cash for proven security)vis the vote (true or false)ris the random blinding factor*represents scalar multiplication on the curve+represents point addition on the curve
Pedersen commitments are homomorphic, meaning the sum of commitments equals the commitment of sums:
C(v1,r1) + C(v2,r2) = C(v1+v2, r1+r2)
This property allows the system to:
- Combine individual vote commitments into a single commitment
- Use the sum of blinding factors to reveal the total vote count
- Maintain individual vote privacy
The project includes several helper utilities to manage the cryptographic operations:
- Creates vote commitments using Pedersen commitment scheme
- Generates secure random blinding factors
- Splits blinding factors into shares for talliers
- Provides utilities for vote submission
- Combines individual vote commitments using homomorphic properties
- Creates and manages Merkle trees for voter verification
- Handles vote bundle submission
- Processes blinding factor shares from voters
- Computes partial sums of blinding factors
- Manages tallier submissions to the contract
- Generates proofs for non-voter reporting
- Verifies voter presence in required voters Merkle tree
- Verifies voter absence in actual voters Merkle tree
- Helps enforce participation by enabling slashing of non-voters
- Provides utilities for independent verification of voter participation
- Must stake minimum 1 ETH to participate
- Maintain minimum available balance
- Can request unregistration with cooldown period
- Subject to slashing for non-participation in rounds
- Collects and submits encrypted vote bundles
- Creates Merkle trees for voter participation
- Locks 0.1 ETH stake during role
- Receives reward upon successful completion
- Process blinding factors for vote counting
- Must be distinct from submitter
- Lock 0.1 ETH stake during role
- Receive rewards upon successful completion
To see a complete example of how a round works, check the test/8_E2E.ts file for end-to-end tests and test/helpers/TestUtils.ts for the setupCompleteRound function.
-
Initialization
- Round created with unique ID (recommended to use a hash that includes a timestamp)
- System verifies sufficient voter participation
- Creator publishes vote description and identifier off-chain
-
Role Assignment
- Submitter position filled first
- Two tallier positions filled (must be different from submitter)
- Stake locks of 0.1 ETH applied for each role
-
Voting Process
- Voters create vote commitments locally (true or false)
- Voters generate random blinding factors
- Voters split blinding factors into shares for talliers
- Voters send commitments to submitter off-chain
- Voters send blinding shares to talliers off-chain
- All voting happens locally without on-chain transactions
-
Vote Collection
- Submitter combines all vote commitments using homomorphic properties
- Submitter creates Merkle trees for required and actual voters
- Required voters tree includes all eligible voters from monitoring contract events
- Actual voters tree includes all who submitted valid commitments to the submitter
- Submitter submits combined commitment and Merkle roots on-chain
- Merkle trees made public for verification
-
Vote Resolution
- Talliers sum their received blinding shares
- Each tallier submits their sum to the contract
- Contract combines sums with vote commitment to reveal result
- Individual votes remain private
-
Finalization
- Vote outcome determined and recorded
- Role participants receive rewards
- Stakes released
- Non-voter reporting enabled
-
Non-Voter Reporting
- Anyone can report non-voters using ObserverUtils
- Must provide two Merkle proofs:
- Voter was in required voters tree
- Voter was not in actual voters tree
- Valid reports result in non-voter being slashed
- Reporter receives portion of slashed stake
stateDiagram-v2
[*] --> Initialized: initializeVotingRound()
Initialized --> SubmitterSelected: volunteerAsSubmitter()
SubmitterSelected --> TallierSelected: volunteerAsTallier() x2
TallierSelected --> BundleSubmitted: submitVoteBundle()
BundleSubmitted --> BlindingSubmitted: submitBlindingSum() x2
BlindingSubmitted --> Finalized: finalizeVote()
Initialized --> Expired: handleExpiredRound()
SubmitterSelected --> Expired: handleExpiredRound()
TallierSelected --> Expired: handleExpiredRound()
BundleSubmitted --> Expired: handleExpiredRound()
BlindingSubmitted --> Expired: handleExpiredRound()
- Node.js (v16 or later)
- npm or yarn
- Git
-
Clone the repository:
git clone https://github.com/bwhdx/ballot-box cd ballot-box -
Install dependencies:
npm install
-
Compile contracts:
npx hardhat compile
-
Start a local node in one terminal and keep it running:
npx hardhat node
⚠️ Keep this terminal open and running -
Open a new terminal, then deploy contracts to the local network:
# Make sure your node terminal is still running npx hardhat run scripts/deploy.ts --network localhostYou should see output like:
Deploying PrivacyPreservingVoting contract... Deploying proxy... PrivacyPreservingVoting deployed to: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 Deployment complete!⚠️ Save the deployed address, you'll need it for future interactions -
Run tests (in the same terminal as step 3):
# Run all tests npx hardhat test # Run specific test file npx hardhat test test/8_E2E.ts
The tests are organized into 8 files that cover all aspects of the system:
test/1_AccountSetup.ts: Initial account setup, voter registration, and basic stake managementtest/2_VoterManagement.ts: Voter lifecycle management including registration, unregistration, and stake updatestest/3_RoundLifecycle.ts: Round creation, state transitions, and lifecycle managementtest/4_RoleManagement.ts: Role assignment, validation, and management for submitters and tallierstest/5_VoteProcessing.ts: Vote submission, processing, and commitment verificationtest/6_RewardsAndSlashing.ts: Economic incentives, reward distribution, and slashing mechanismstest/7_Security.ts: Security features, attack vectors, and protection mechanismstest/8_E2E.ts: End-to-end voting process and system integration tests
- Minimum stake requirements protect against Sybil attacks
- Role-based stake locking ensures commitment
- Slashing mechanisms discourage malicious behavior
- Cooldown periods prevent quick exits
- Separation of duties between submitters and talliers
- Generator points from proven secure system (Tornado Cash)
- Voter Registration: 1 ETH minimum stake
- Role Locks: 0.1 ETH per role
- Non-voter Slash: 0.05 ETH
- Rewards:
- Submitter: 0.03 ETH
- Talliers: 0.03 ETH (split between talliers)
- Treasury Fee: 0.015 ETH
- OpenZeppelin for secure contract implementations
- Hardhat development environment
- Ethereum community for privacy-preserving voting research
- Tornado Cash for proven generator points
This implementation contains known vulnerabilities for educational purposes. The following analysis assumes honest behavior from most participants:
-
Voters submit all required data to submitters and talliers:
- Their address to the submitter
- Their vote commitment to the submitter
- Their blinding factor shares to the talliers
-
Submitters create valid vote totals and merkle trees:
- Include only valid voters in required voters merkle root
- Include all participating voters in actual voters merkle root
- Include all vote commitments when creating the vote total point
- Provide public access to merkle trees for verification
-
Talliers correctly sum and submit blinding factor shares received from voters
-
No Sybil attacks - all addresses represent unique individuals
Issue: Voters can submit Pedersen commitments with values other than 0 (false) or 1 (true).
Attack: A malicious voter could commit to a value of 2, 3, or any integer, effectively casting multiple votes while appearing to cast only one. Since individual votes remain private, this manipulation is undetectable.
Impact: Vote outcomes can be manipulated without detection. For example, in a 50-voter election where 25 vote "no" and 24 vote "yes", a single malicious voter could cast a value of 2 to tip the result.
Root Cause: The system lacks range proofs to constrain vote values to the valid {0,1} set.
Issue: The last tallier to submit can preview vote results before submitting their blinding factor share.
Attack: By monitoring other talliers' submissions and computing partial results, the final tallier knows the outcome before finalizing it. They could selectively abstain if the result is undesirable (accepting the slashing penalty).
Impact: Vote outcomes could be manipulated through selective tallier participation.
To address these vulnerabilities:
-
Implement Zero-Knowledge Range Proofs: Require voters to prove their commitments contain values in {0,1} without revealing the actual vote.
-
Threshold Encryption: Encrypt blinding factor shares with the contract's public key, preventing premature result calculation.
-
Validator Consensus System:
- Replace submitter/tallier roles with a unified validator role
- Require ⅔ consensus on vote validity and merkle tree construction
- Implement random validator selection
-
Enhanced Cryptographic Guarantees:
- Require signed vote commitments to ensure data integrity
- Implement commitment-bound zero-knowledge proofs for blinding factor shares
- Add consensus requirements for vote commitment sums and tree roots
Potential enhancements for the system:
- Optimize gas usage while maintaining code readability and maintainability
- Modularize functionality using inheritance and interfaces:
- Split registration into a separate contract
- Split stake management into a separate contract
- Split round logic into a separate contract
- Implement best practices & patterns, such as Storage pattern to prevent state corruption during upgrades
- Expand test coverage
- Add comprehensive inline documentation
- Enhance error handling and logging
- Add treasury management features
- Implement security fixes for known vulnerabilities