A Rust-based implementation of a fully on-chain, anonymous voting protocol using Proof-of-Burn and zk-SNARKs. Voters burn tokens to unspendable addresses, generate a zero-knowledge proof, and submit their vote on Ethereum-compatible chains without sacrificing privacy or verifiability.
- Fully On-Chain: No trusted third parties or off-chain tallying—everything happens in smart contracts.
- Anonymous & Coercion-Resistant: Each burn address is a hash commitment (binding and hiding). Voters can cast multiple unlinkable burns to override coerced votes.
- Flexible Voting Schemes: Supports majority voting, token-weighted, quadratic, ranked-choice, or open-content ballots via on-chain tally logic.
- Lightweight ZKPs: Uses Circom + Groth16 for succinct proofs; avoids heavy homomorphic encryption or MPC overhead.
- Rust CLI: Convenient command-line interface powered by
structopt
andtokio
.
If you have used this repo to develop a research work or product, please cite our paper:
@misc{cryptoeprint:2025/1022,
author = {Stefan Dziembowski, Shahriar Ebrahimi, Haniyeh Habibi, Parisa Hassanizadeh and Pardis Toolabi},
title = {Burn Your Vote: Decentralized and Publicly Verifiable Anonymous Voting at Scale},
howpublished = {Cryptology {ePrint} Archive, Paper 2025/1022},
year = {2025}
}
.
├── Cargo.toml
├── README.md
├── src
│ ├── main.rs # CLI entry point
│ ├── circuits # Circom circuit handlers
│ ├── commands # vote, tally, demo, onchain_demo handlers
│ ├── utils # configuration parsing, helper functions
│ └── db # simple on-disk storage for ceremony state
└── contracts # Solidity contracts (verifier + voting logic)
└── circuits # circom circuits (vote + burn address + nullifier + mpt)
Before setting up the project, ensure the following tools are installed:
- Node.js (>=16.0.0) - Install Node.js
- Rust (for Cargo) - Install Rust
- Homebrew (for macOS) - Install Homebrew
- Circom - Install Circom
To set up the project, follow these steps to get the project up and running on your local machine.
-
Clone the Repository:
git clone [email protected]:zero-savvy/burn-to-vote.git cd burn-to-vote
-
Install Project Dependencies:
npm run install-deps
This will:
- Install all Node.js dependencies including Circomlib, ganache-cli and snarkjs.
- Run additional setup scripts to install Rapidsnark in the circuits folder.
-
Start Ganache Locally:
ganache-cli export PRIVATE_KEY=[PRIVATE-KEY]
This will start a local blockchain instance for testing.
To vote you need to generate an new ceremony or use an existing one. If you already have the voting ceremony id you can skip this step.
Configure a new voting instance with default config:
network options: Ganache, Sepolia, Ethereum
cargo run -- initiate --network [NETWORK]
- Generates initial parameters
Config {
network,
ceremony_id,
chain_id,
votingDeadline,
tallyDeadline,
stateRoot,
result,
white_list,
yesVotes,
noVotes,
finilized,
}
Warning: this command might take (~ 1 hour) to run. Burn-to-vote flow: compute a burn address, nullifier, burn eth and generate a proof:
cargo run -- vote --amount [AMOUNT] --vote [VOTE] --revote [REVOTE-FLAG] --private-key [PRIVATE-KEY] --ceremony-id [CEREMONY-ID]
AMOUNT
: The amout of ETH to burn.VOTE
: vote value (e.g., 0 or 1 for yes/no).REVOTE
: revote flag value (e.g., 0 1 for revoting).PRIVATE-KEY
: private key (for ZK burn transaction).CEREMONY-ID
: The unique ceremony identifier.
If no ceremony is providede the vote is applied to the latest generated ceremony.
To get the list of available ceremonies:
cargo run -- ceremonies
Calculates and prints the result of ceremony if the tally time has passed:
cargo run -- tally --ceremony-id [CEREMONY-ID]
CEREMONY-ID
: The unique ceremony identifier.
if no ceremony id is providede the tally is applied on the latest generated ceremony.
Runs an in-memory ceremony without on-chain dependencies (for testing):
- compiles the vote circuit (~ 40 min)
- creates a burn address
- creates a nullifier
- burns a predetermined eth amount
- adds the data to merkle tree
- creates the vote circuit inputs, zkey, verification key (~ 1 hour)
- creates the vote proof
- verifies the proof off-chain
cargo run -- demo [PRIVATE-KEY]
Demonstrates fully on-chain interactions :
- deploys the voting smart contract
- submits a pregenerated test vote proof
- returns the tally results
make contracts
cargo run -- onchain-demo [PRIVATE-KEY]
The smart contracts in contracts/
implement a factory pattern for deploying voting instances:
The factory contract (contracts/src/VotingFactory.sol
) is responsible for deploying new voting instances with the following parameters:
- Verifier Address: Deployed Groth16 verifier (BN254)
- Voting Deadline: UNIX timestamp to lock voting
- Tally Deadline: After this, anyone can call
tallyVotes()
- Merkle Root: Allow-list root for eligible voters
- State Root: Ethereum state root at voting end (for verifying burn balances)
- Ceremony ID: Unique voting identifier
- Salt: Unique salt for deterministic contract address generation
Key functions:
deployVotingContract(salt, verifier, merkleRoot, stateRoot, votingDeadline, tallyDeadline, ceremonyId)
: Deploys a new voting contract instancegetVotingContractAddress(salt)
: Returns the deterministic address for a voting contract given its salt
Each deployed voting contract instance (contracts/src/Voting.sol
) implements the core voting logic:
Key functions:
submitVote(proofA, proofB, proofC, [nullifier, voteValue, revoteFlag, stateRoot, merkleProof, ceremonyId])
: Submits a vote with a zero-knowledge proofsubmitRevote(...)
: Overwrites a previous vote if the nullifier matchestallyVotes()
: Computes and publishes the final outcome after the tally deadline
The factory pattern allows for:
- Deterministic contract addresses based on salt
- Multiple concurrent voting instances
- Gas-efficient deployment of new voting contracts
- Easy tracking of all deployed voting instances
Under circuits/
:
Vote.circom
:- Computes burn address:
address == H(secret ∥ ceremonyID ∥ vote ∥ blindingFactor)
. - Computes nullifier:
address == H(secret ∥ ceremonyID ∥ blindingFactor)
. - Verifies Merkle Patricia inclusion (
stateRoot
,accountRLP
,accountProof
).
- Computes burn address:
rlp.circom
: Supporting subcircuits.
- Scalability: Supports >1 million simulated voters with constant-time tallying.
- Gas Costs: Proof verification ~200k gas per vote; tallying is O(1).
- Proof Generation: ≤2s per witness on a modern CPU.
This project is licensed under the MIT License - see the LICENSE file for details.