A secure, scalable voting application built on Solana using the Anchor framework. Implements advanced D21 protocol with duplicate vote prevention, flexible poll management, and efficient bitmap-based vote tracking.
Experience D21Voting in action: https://d21voting.vercel.app/
- ๐ Secure Voting - Prevents duplicate votes using advanced bitmap tracking
- ๐ฏ Flexible Polls - Configurable candidate limits and voting quotas
- ๐ Authority Control - Poll creators maintain full control over their polls
- โก Efficient Storage - Optimized account sizing and memory usage
- ๐ง D21 Protocol - Advanced voting with multiple votes per voter
- ๐ Production Ready - Fully tested and deployed on Solana
2CNQMKvkPPfgiZFKJar6gyWc6bquTV2jW7NEHMfynLBs
graph TD
A[Poll Creator] --> B[Create Poll]
B --> C[Poll Account]
A --> D[Add Candidates]
D --> E[Candidate Accounts]
F[Voter] --> G[Init Vote Record]
G --> H[Vote Record Account]
F --> I[Cast Votes]
I --> C
I --> H
A --> J[Close Poll]
J --> C
d21voting-backend/
โโโ ๐ Anchor.toml # Project configuration
โโโ ๐ฆ Cargo.toml # Rust dependencies
โโโ ๐ฑ package.json # Node.js dependencies
โโโ โ๏ธ tsconfig.json # TypeScript configuration
โโโ ๐๏ธ programs/
โ โโโ d21voting/
โ โโโ ๐ฆ Cargo.toml # Program-specific dependencies
โ โโโ ๐ src/
โ โโโ ๐ lib.rs # Main program entry point
โ โโโ ๐๏ธ state.rs # Account structures
โ โโโ ๐ contexts.rs # Account validation
โ โโโ โ errors.rs # Custom error types
โ โโโ ๐ ๏ธ utils.rs # Utility functions
โ โโโ ๐ instructions/ # Instruction handlers
โ โโโ mod.rs
โ โโโ create_poll.rs
โ โโโ add_candidate.rs
โ โโโ init_vote_record.rs
โ โโโ cast_votes.rs
โ โโโ close_poll.rs
โโโ ๐งช tests/ # Integration tests
โโโ ๐ migrations/ # Deployment scripts
โโโ ๐ฏ target/ # Build artifacts
Creates a new voting poll with specified parameters.
pub fn create_poll(
ctx: Context<CreatePoll>,
poll_id: u64,
title: String,
max_candidates: u16,
max_votes_per_voter: u16,
max_title_bytes: u32,
) -> Result<()>Parameters:
poll_id- Unique identifier for the polltitle- Poll title (with size validation)max_candidates- Maximum number of candidates allowedmax_votes_per_voter- Maximum votes each voter can castmax_title_bytes- Reserved space for title
Adds a candidate to an existing poll.
pub fn add_candidate(
ctx: Context<AddCandidate>,
index: u16,
name: String,
max_name_bytes: u32,
) -> Result<()>Constraints:
- Poll must be open
- Index must equal current candidate count
- Candidate count must be below maximum
Creates a vote record for a voter in a specific poll.
pub fn init_vote_record(ctx: Context<InitVoteRecord>) -> Result<()>Features:
- One-time initialization per voter per poll
- Automatic bitmap sizing based on poll configuration
- Voter pays for account creation
Allows voters to cast votes for candidates.
pub fn cast_votes(ctx: Context<CastVotes>, indices: Vec<u16>) -> Result<()>Advanced Features:
- Duplicate Prevention - Uses bitmap to prevent voting for same candidate twice
- Quota Management - Respects max_votes_per_voter limit
- Batch Voting - Multiple candidates in single transaction
- Smart Deduplication - Handles duplicate indices within same call
Closes a poll to prevent further voting.
pub fn close_poll(ctx: Context<ClosePoll>) -> Result<()>Effect: Sets is_open = false, blocking all future votes
pub struct Poll {
pub authority: Pubkey, // Poll creator (32 bytes)
pub poll_id: u64, // Unique identifier (8 bytes)
pub is_open: bool, // Poll status (1 byte)
pub max_votes_per_voter: u16, // Voting limit (2 bytes)
pub max_candidates: u16, // Candidate limit (2 bytes)
pub candidate_count: u16, // Current count (2 bytes)
pub title: String, // Poll title (4 + length bytes)
pub vote_counts: Vec<u64>, // Vote counts per candidate
}pub struct Candidate {
pub poll: Pubkey, // Associated poll (32 bytes)
pub index: u16, // Candidate index (2 bytes)
pub name: String, // Candidate name (4 + length bytes)
}pub struct VoteRecord {
pub poll: Pubkey, // Associated poll (32 bytes)
pub voter: Pubkey, // Voter's public key (32 bytes)
pub used_votes: u16, // Votes already cast (2 bytes)
pub voted_bitmap: Vec<u8>, // Bitmap tracking voted candidates
}- Poll Seeds:
[b"poll", authority, poll_id] - Candidate Seeds:
[b"candidate", poll, index] - Vote Record Seeds:
[b"vote", poll, voter] - Deterministic - Same inputs always produce same addresses
- Collision Resistant - Unique combinations prevent conflicts
- Authority Validation - Only poll creators can modify their polls
- Poll Status Checks - Closed polls reject all modifications
- Voter Verification - Vote records are tied to specific voters
- Sequential Constraints - Candidates must be added in order
- Input Validation - All parameters checked before processing
- Overflow Protection - Uses checked arithmetic operations
- Size Constraints - Pre-allocates vectors to prevent stack issues
- Duplicate Prevention - Bitmap-based vote tracking
// Calculate bitmap size in bytes
fn bitset_len_bytes(max_candidates: u16) -> usize {
((max_candidates as usize) + 7) / 8
}
// Check if candidate was voted for
fn bit_is_set(bitmap: &Vec<u8>, index: u16) -> bool {
let byte = index as usize / 8;
let bit = index as usize % 8;
(bitmap[byte] & (1u8 << bit)) != 0
}
// Mark candidate as voted for
fn set_bit(bitmap: &mut Vec<u8>, index: u16) {
let byte = index as usize / 8;
let bit = index as usize % 8;
bitmap[byte] |= 1u8 << bit;
}- Intra-transaction - Removes duplicate indices within same call
- Cross-transaction - Bitmap prevents voting for same candidate twice
- Efficient - O(n) complexity with BTreeSet for deduplication
pub enum D21Error {
ZeroCandidatesNotAllowed, // max_candidates must be > 0
ZeroVotesPerVoter, // max_votes_per_voter must be > 0
PollClosed, // Poll is not accepting votes
Unauthorized, // Insufficient permissions
MaxCandidatesReached, // Cannot add more candidates
NameTooLongAtInit, // Candidate name exceeds limit
TitleTooLongAtInit, // Poll title exceeds limit
NoVotesSubmitted, // Empty vote indices
NoNewVotes, // All votes are duplicates/already cast
VotesQuotaExceeded, // Exceeds max_votes_per_voter
InvalidCandidateIndex, // Index out of bounds
Overflow, // Arithmetic overflow
MismatchedPollInRecord, // Vote record links to wrong poll
}- Rust (latest stable)
- Solana CLI
- Anchor CLI
- Node.js (v16 or later)
- Yarn package manager
# Clone the repository
git clone https://github.com/mapfumo/d21voting-anchor.git
cd d21voting-anchor
# Install dependencies
yarn install
# Build the program
anchor build
# Run tests
yarn testconst pollId = new BN(Date.now());
const title = "Favorite Programming Language";
const maxCandidates = 5;
const maxVotesPerVoter = 3;
const maxTitleBytes = title.length + 10;
await program.methods
.createPoll(pollId, title, maxCandidates, maxVotesPerVoter, maxTitleBytes)
.accounts({
authority: wallet.publicKey,
poll: pollPda,
systemProgram: SystemProgram.programId,
})
.rpc();const candidateIndex = poll.account.candidateCount;
const candidateName = "Rust";
const maxNameBytes = candidateName.length + 5;
await program.methods
.addCandidate(candidateIndex, candidateName, maxNameBytes)
.accounts({
poll: pollPda,
candidate: candidatePda,
authority: wallet.publicKey,
systemProgram: SystemProgram.programId,
})
.rpc();const voteIndices = [0, 2, 4]; // Vote for candidates 0, 2, and 4
await program.methods
.castVotes(voteIndices)
.accounts({
voter: wallet.publicKey,
poll: pollPda,
voteRecord: voteRecordPda,
})
.rpc();# Install dependencies
yarn install
# Run all tests
yarn test
# Run specific test file
yarn test tests/d21voting.ts- Unit Tests - Individual instruction functionality
- Integration Tests - End-to-end voting workflows
- Error Cases - All error conditions and edge cases
- Security Tests - Authority validation and access control
anchor buildanchor deploy# Update Anchor.toml cluster to "devnet"
anchor deploy --provider.cluster devnetanchor verify <PROGRAM_ID>- Efficient Storage - Pre-allocated vectors prevent reallocation
- Bitmap Operations - O(1) vote checking and setting
- Minimal Instructions - Optimized transaction structure
- Configurable Limits - Adjustable candidate and vote limits
- Batch Operations - Multiple votes in single transaction
- Memory Efficient - Dynamic sizing based on actual needs
- Poll Templates - Reusable poll configurations
- Advanced Voting - Weighted voting, ranked choice
- Real-time Updates - WebSocket integration
- Analytics - Voting patterns and statistics
- Multi-signature - Collaborative poll management
- Batch Operations - Multiple candidates in single transaction
- Compression - Efficient data storage algorithms
- Caching - Frequently accessed data optimization
We welcome contributions! Please see our Contributing Guidelines for details.
- Fork the repository
- Clone your fork locally
- Install dependencies:
yarn install - Build the program:
anchor build - Run tests:
yarn test - Submit a pull request
- Follow Rust formatting guidelines (
cargo fmt) - Add comprehensive documentation
- Include error handling
- Write tests for new features
- Update documentation as needed
- Anchor Book - Official Anchor documentation
- Solana Cookbook - Solana development recipes
- Solana Program Library - Standard Solana programs
- Anchor Discord - Get help and discuss
- Solana Discord - Solana community
- Stack Exchange - Q&A platform
This project is licensed under the MIT License - see the LICENSE file for details.
- Anchor Team - For the excellent framework
- Solana Team - For the blockchain platform
- Community - For feedback and contributions
Program ID: 2CNQMKvkPPfgiZFKJar6gyWc6bquTV2jW7NEHMfynLBs
Version: 1.0.0
Status: โ
Production Ready
Frontend: โ
Live on Vercel
Last Updated: August 2025